[
https://issues.apache.org/jira/browse/SOLR-12121?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16592255#comment-16592255
]
Jan Høydahl commented on SOLR-12121:
------------------------------------
I got it working with a custom Principal object carrying the token. But I had
to "help" the Principal object jump from the original external request over to
all the inter-node request objects, so that it could be picked up by the
interceptor. Here are some code fragments of what I did:
*JWTAuthPlugin#authenticate():*
{code:java}
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
@Override
public Principal getUserPrincipal() {
return authResponse.getPrincipal(); // Custom JWTPrincipal carrying the
token and claims
}
};{code}
*HttpShardHandler#submit():*
This is the code called by each handler to spawn a distributed request
{code:java}
// If request has a Principal (authenticated user), extract it for passing on
to the new shard request
SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
final Principal userPrincipal = requestInfo == null ? null :
requestInfo.getReq().getUserPrincipal();{code}
*SolrRequest:*
SolrRequest is the base class of QueryRequest and did not have a userPrincipal
member since this is a SolrJ request usually constructed on the client side and
thus there would be no authenticated user. But when this object is used for sub
requests based on an original user request, it makes sense to store the
Principal here. In this case Principal can be any sub class carrying arbitrary
data about the user, which can be picked up by AuthPlugin's HttpClient
interceptor to set a request header or similar.
{code:java}
// This user principal is typically used by Auth plugins during
distributed/sharded search
private Principal userPrincipal;
public void setUserPrincipal(Principal userPrincipal) {
this.userPrincipal = userPrincipal;
}
public Principal getUserPrincipal() {
return userPrincipal;
}
{code}
*Callable executed by completionService in new threads:*
HttpShardHandler's submit() creates one Callable per shard and then sends them
to completion service to execute in a new thread pool. We pick up the Principal
object from the (final) userPrincipal variable populated above, and set the
principal on the new QueryRequest (sub class of SolrRequest) object.
{code:java}
Callable<ShardResponse> task = () -> {
...
QueryRequest req = makeQueryRequest(sreq, params, shard);
req.setMethod(SolrRequest.METHOD.POST);
req.setUserPrincipal(userPrincipal); // New line to set original principal on
request object{code}
*HttpSolrClient#request():*
In the request() method of the HttpSolrClient, we pick up userPrincipal from
the incoming SolrRequest and send it to executeMethod()
{code:java}
public NamedList<Object> request(final SolrRequest request, final
ResponseParser processor, String collection)
...
return executeMethod(method, request.getUserPrincipal(), processor,
isV2ApiRequest(request));
{code}
*HttpSolrClient#executeMethod:*
In executeMethod we now put Principal object on the HttpContext, which is part
of the execute() call
{code:java}
protected NamedList<Object> executeMethod(HttpRequestBase method, Principal
userPrincipal, final ResponseParser processor, final boolean isV2Api) throws
SolrServerException {
...
// Execute the method.
HttpClientContext httpClientRequestContext =
HttpClientUtil.createNewHttpClientRequestContext();
if (userPrincipal != null) {
// Normally the context contains a static userToken to enable reuse
resources.
// However, if a personal Principal object exists, we use that instead,
also as a means
// to transfer authentication information to Auth plugins that wish to
intercept the request later
httpClientRequestContext.setUserToken(userPrincipal);
}
final HttpResponse response = httpClient.execute(method,
httpClientRequestContext);
{code}
*JWTAuthPlugin#getHttpClientBuilder:*
Back in our auth plugin we register an interceptor that eventually picks up the
execute call from above. It lifts the httpContext, and if it contains a
JWTPrincipal, then it can read the token from there, and set the Authorization
header. If there is no JWTPrincipal, we delegate to PKI plugin and let it do
its thing.
{code:java}
@Override
public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder
builder) {
// Register interceptor for inter-node requests, that delegates to PKI if
JWTPrincipal is not found on http context
HttpClientUtil.addRequestInterceptor(interceptor);
return builder;
}
// The interceptor class that adds correct header or delegates to PKI
private class PkiDelegationInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) throws
HttpException, IOException {
if (context instanceof HttpClientContext) {
HttpClientContext httpClientContext = (HttpClientContext) context;
if (httpClientContext.getUserToken() instanceof JWTPrincipal) {
JWTPrincipal jwtPrincipal = (JWTPrincipal)
httpClientContext.getUserToken();
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " +
jwtPrincipal.token);
log.debug("Set JWT header on inter-node request");
return;
}
}
if (coreContainer.getPkiAuthenticationPlugin() != null) {
log.debug("Inter-node request delegated from JWTAuthPlugin to
PKIAuthenticationPlugin");
coreContainer.getPkiAuthenticationPlugin().setHeader(request);
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"JWTAuthPlugin wants to delegate inter-node request to PKI, but PKI
plugin was not initialized");
}
}
}
{code}
Well, it works, I have a passing integration test on a 2-node 2-shard cluster.
Other things to note:
* Admin requests such as create collection etc will always fallback to PKI;
that's fine, I don't have a requirement for end-user auth on such inter-node
reqs
* Have not yet tested this with distributed Update - I believe we need a
similar Principal transfer in {{DistributedUpdateProcessor}}
* I think we could be smarter with the PKI thing and interceptors. What if PKI
always registered its interceptor, but the very first thing it would do is to
ask the registered AuthPlugin whether it wants to attempt handling inter-node
requests, and then call some new API on the plugin to allow it to do so, and
only if the other plugin gave up, PKI would set its header. This would lead to
cleaner code in plugins, and if PKI later changes its interceptor, we won't
need to change a bunch of other plugins as well. Pseudo code:
{code:java}
if (coreContainer.getAuthenticationPlugin() instanceof HttpClientBuilderPlugin)
{
if (!coreContainer.getAuthenticationPlugin().interceptRequest(request,
context)) {
// add our header
}
} // else do nothing
{code}
Other request flows we should check?
> JWT Authentication plugin
> -------------------------
>
> Key: SOLR-12121
> URL: https://issues.apache.org/jira/browse/SOLR-12121
> Project: Solr
> Issue Type: New Feature
> Security Level: Public(Default Security Level. Issues are Public)
> Components: Authentication
> Reporter: Jan Høydahl
> Assignee: Jan Høydahl
> Priority: Major
> Fix For: master (8.0), 7.5
>
> Time Spent: 10m
> Remaining Estimate: 0h
>
> A new Authentication plugin that will accept a [Json Web
> Token|https://en.wikipedia.org/wiki/JSON_Web_Token] (JWT) in the
> Authorization header and validate it by checking the cryptographic signature.
> The plugin will not perform the authentication itself but assert that the
> user was authenticated by the service that issued the JWT token.
> JWT defined a number of standard claims, and user principal can be fetched
> from the {{sub}} (subject) claim and passed on to Solr. The plugin will
> always check the {{exp}} (expiry) claim and optionally enforce checks on the
> {{iss}} (issuer) and {{aud}} (audience) claims.
> The first version of the plugin will only support RSA signing keys and will
> support fetching the public key of the issuer through a [Json Web
> Key|https://tools.ietf.org/html/rfc7517] (JWK) file, either from a https URL
> or from local file.
--
This message was sent by Atlassian JIRA
(v7.6.3#76005)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]