Is it possible to validate a JWT token created fro

2019-08-19 08:01发布

问题:

I have a Spring Security OAuth2 server that generates JWT tokens for frontend applications.

This tokens will be sent in the calls to the backends, passing through an API Gateway (WSO2 API Manager).

What I would like is to have the backend APIs registered in WSO2 and be able to validate that externally generated JWT token.

Is this possible? Can you provide a sample of the different places of the WSO2 APIM that would need to be configured to include this logic?

NOTICE: The WSO2 will never need to create the token, it'll always have been created previously, it only needs to validate it.

回答1:

After a lot of trial and error and some help from Stackoverflow (kudos to Bee) this is my working solution. I hope it helps others since it was really tricky to make it work:

1. Implement a JWTAuthHandler to validate the JWT tokens:

public class JwtAuthHandler extends AbstractHandler {

    private final PublicKeyFactory pkf = new PublicKeyFactory();
    private final JwtVerifier jwtVerifier = new JwtVerifier();

    @Override
    public boolean handleRequest(MessageContext messageContext) {

        try {
            final String jwtToken = getJwtTokenFromHeaders(messageContext).replace("Bearer ", "");

            SignedJWT signedJwt = SignedJWT.parse(jwtToken);
            final JSONObject payload = signedJwt.getPayload().toJSONObject();
            final JSONObject environment = (JSONObject)payload.get("environment");

            PublicKey publicKey = readPublicKey();

            JWSVerifier verifier = new RSASSAVerifier(((RSAPublicKey) publicKey));
            final boolean signatureVerification = signedJwt.verify(verifier)

            if (signatureVerification) {
                AuthenticationContext authContext = new AuthenticationContext();
                authContext.setAuthenticated(true);
                if (isProductionRequest(environment)) {
                    authContext.setKeyType(APIConstants.API_KEY_TYPE_PRODUCTION);
                } else {
                    authContext.setKeyType(APIConstants.API_KEY_TYPE_SANDBOX);
                }
                APISecurityUtils.setAuthenticationContext(messageContext, authContext, "Authorization");
            } else {
                LOG.debug("handleRequest() - Sending 401 Unauthorized");
                Utils.sendFault(messageContext, 401);
            }
            return signatureVerification;
        } catch (Exception e) {
            e.printStackTrace();
            Utils.sendFault(messageContext, 500);
            return false;
        }
    }

    @Override
    public boolean handleResponse(MessageContext messageContext) {
        return true;
    }

    private String getJwtTokenFromHeaders(MessageContext messageContext) {
        Map headers = (Map) ((Axis2MessageContext) messageContext).getAxis2MessageContext().
                getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
        return (String) headers.get("Authorization");
    }

    private boolean isProductionRequest(JSONObject environment) {
        return environment != null && environment.equals("pro");
    }

}

2. Override your API definition (/repository/deployment/server/synapse-configs/default/api/yourapi.xml) to use the jwt handler and remove the APIAuthenticationHandler and the ThrottleHandler (the latter needs to be removed because of a well known bug for non-oauth2-authenticated apis):

It should have something like this:

<handlers>
  <handler class="com.codependent.JwtAuthHandler"/>
  <handler class="org.wso2.carbon.apimgt.gateway.handlers.common.APIMgtLatencyStatsHandler"/>
  <handler class="org.wso2.carbon.apimgt.gateway.handlers.security.CORSRequestHandler">
     <property name="apiImplementationType" value="ENDPOINT"/>
  </handler>
  <handler class="org.wso2.carbon.apimgt.gateway.handlers.analytics.APIMgtUsageHandler"/>
  <handler class="org.wso2.carbon.apimgt.gateway.handlers.analytics.APIMgtGoogleAnalyticsTrackingHandler">
     <property name="configKey" value="gov:/apimgt/statistics/ga-config.xml"/>
  </handler>
  <handler class="org.wso2.carbon.apimgt.gateway.handlers.ext.APIManagerExtensionHandler"/>


</handlers>

IMPORTANT:

  1. The processing backend is usually derived (in normal OAuth2 requests) from the OAuth2 access token. Since here we replaced it, WSO2 can't determine which environment to invoke so it'll call PRODUCTION as default. To work around this, insert some extra field in your JWT, in my case environment, that helps you decide. Then, create an AuthenticationContext with the appropiate environment as shown. That's it!

  2. If you directly edit yourapi.xml descriptor it'll be replaced the next time you publish it. To automate its generation edit the velocity template (/repository/resources/api_templates/velocity_template.xml). In my case I only want it to apply to some applications, so I use a tag (jwt-auth) to select them.

velocity_template.xml:

<handlers xmlns="http://ws.apache.org/ns/synapse">
#if($apiObj.tags.contains("jwt-auth"))
    <handler class="com.codependent.JwtAuthHandler"/>
#end
#foreach($handler in $handlers)
    #if((($handler.className != "org.wso2.carbon.apimgt.gateway.handlers.security.APIAuthenticationHandler") && 
         ($handler.className != "org.wso2.carbon.apimgt.gateway.handlers.throttling.ThrottleHandler")) || 
            !($apiObj.tags.contains("jwt-auth")))
    <handler xmlns="http://ws.apache.org/ns/synapse" class="$handler.className">
        #if($handler.hasProperties())
            #set ($map = $handler.getProperties() )
            #foreach($property in $map.entrySet())
        <property name="$!property.key" value="$!property.value"/>
            #end
        #end
    </handler>
    #end
#end
</handlers>


回答2:

Indeed you could write a custom handler to authenticate on your own (basic authentication, jwt bearer). Good job finding that quickly. Maybe as an improvement you could cache the validated jwt token (or jwt hash) as the validation may take some time and performance.

As a default solution (without any customization) you could use JWT grant exchanging a token from a trusted IdP for an internal APIM token.