[ 
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]

Reply via email to