0

I'm having trouble validating an access token I receive from Azure in my Java web app. The jose4j library's signature.verifySignature() is simply returning false. Could someone please help me understand what I'm doing wrong?

I have successfully configured a front-facing web app to login via Azure Active Directory, upon which it receives an access token in the form of a signed Json Web Token. I intend to attach that JWT in a header, "Authoriation": "token <signed-access-token>", in all requests to my other database-facing web apps. I would like to create a Spring Filter to validate the JWT's signature for each of these database-facing web apps, to ensure that the token was in fact issued by Azure.

I have the code for my little proof of concept in this github repo's branch: repo. But here's my filter:

package com.doug.example.oauthclient.microservice.config;

//...
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.jwk.VerificationJwkSelector;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.JoseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.GenericFilterBean;

public class AzureJwtFilter extends GenericFilterBean {
    private final static Logger LOG = LoggerFactory.getLogger(AzureJwtFilter.class);

    private final static String AUTHORIZATION_HEADER_NAME = "Authorization";
    private final static String TOKEN_PREFIX = "token ";

    private String azurePublicKeyUrl = "https://login.microsoftonline.com/<my-ad>.onmicrosoft.com/discovery/v2.0/keys";

    private RestOperations restTemplate = new RestTemplate();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) {

        HttpServletRequest httpRequest = (HttpServletRequest)request;
        if(httpRequest.getHeader(AUTHORIZATION_HEADER_NAME) == null ||
                !httpRequest.getHeader(AUTHORIZATION_HEADER_NAME).startsWith(TOKEN_PREFIX) ) {
            throw new AuthenticationCredentialsNotFoundException("Missing or Invalid Authorization Header in Request");
        }

        String authorizationToken = httpRequest.getHeader(AUTHORIZATION_HEADER_NAME).replace(TOKEN_PREFIX, "");
        String[] tokenParts = authorizationToken.split("\\.");
        if(tokenParts.length != 3) {
            throw new InvalidTokenException("The authorization token did not have 3 parts");
        }

        try {
            String publicKeySetJson = restTemplate.getForObject(
                    new URI(azurePublicKeyUrl), String.class);
            JsonWebKeySet publicKeySet = new JsonWebKeySet(publicKeySetJson);
            JsonWebSignature signature = new JsonWebSignature();
            signature.setAlgorithmConstraints(new AlgorithmConstraints(
                    AlgorithmConstraints.ConstraintType.WHITELIST,
                    AlgorithmIdentifiers.RSA_USING_SHA256));
            signature.setCompactSerialization(authorizationToken);
            VerificationJwkSelector publicKeySelector = new VerificationJwkSelector();
            JsonWebKey jsonWebKey = publicKeySelector.select(
                    signature, publicKeySet.getJsonWebKeys());
            signature.setKey(jsonWebKey.getKey());
            if(!signature.verifySignature()) { // <<== ALWAYS FAILS VERIFICATION :(
                throw new RuntimeException("JSON Web Token Signature Invalid");
            }
        } catch (URISyntaxException e) {
            LOG.error("Invalid URL \"" + azurePublicKeyUrl + "\"", e);
        } catch (JoseException e) {
            LOG.error("An error occurred when validating the JWT signature", e);
            throw new RuntimeException(e);
        }

        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }
}

Here's what the Base64 decoded token metadata looks like:

{"typ":"JWT","nonce":"AQABAAAAAADX8GCi6Js6SK82TsD2Pb7r9xWDjnazKO0nBJFdLLawrH4SsyXGtZpR4VSgvoX7ADMIjFSLUAOhd_xJnYCQw85rt3-pFp1UoMW8B9zL3Mjp6SAA","alg":"RS256","x5t":"iBjL1Rcqzhiy4fpxIxdZqohM2Yk","kid":"iBjL1Rcqzhiy4fpxIxdZqohM2Yk"}

Here's what the Base64 decoded Token Body looks like:

{"aud":"https://graph.microsoft.com","iss":"https://sts.windows.net/.../","iat":12345,"nbf":12345,"exp":1528136094,"acr":"1","aio":"Y2dg.../33rf...","amr":["pwd"],"app_displayname":"localhost","appid":"...","appidacr":"1","family_name":"Snoop","given_name":"Dougg","ipaddr":"...","name":"Snoop, Dougg - Dougg","oid":"...","onprem_sid":"...","platf":"3","puid":"...","scp":"User.Read","signin_state":["inknownntwk","kmsi"],"sub":"...","tid":"...","unique_name":"...","upn":"...","uti":"...","ver":"1.0"}

Now I have seen some mention of how the nonce might throw off validation (link), but I don't understand how that would happen. I mean the first 2 parts of the JWT are signed, why would the properties they contain affect the signature's validity? Unless Microsoft is signing the token and then adding in the nonce after the signature is already computed?

SnoopDougg
  • 1,467
  • 2
  • 19
  • 35
  • Since I can't verify the token's signature, I've been sending it to the microsoft graph api as a bearer token to verify it (https://graph.microsoft.com/v1.0/me/)... – SnoopDougg Jun 07 '18 at 17:35
  • did you ever figure this out? I'm experiencing the same thing; if I set the resource to the App ID, I am able to verify the token. But if I set the resource to graph.microsoft.com, I am not able to verify the token. – Brandon Smith Dec 20 '18 at 15:34
  • Sorry, it's been a while since I messed with this, but what do you mean by `the resource`? – SnoopDougg Dec 20 '18 at 21:52
  • With AD v1.0, you pass a resource in the params for fetching an access token; the resource is equivalent to the aud (audience). – Brandon Smith Dec 21 '18 at 15:45
  • Oh interesting, I guess I wasn't using v1.0, it looks like I'm using v2.0, and the access token I get back when I include my App ID is the one I can't validate. In version 1.0 you're able to verify the app-specific access token? The POST request I send for the Access token is to `https://login.microsoftonline.com//oauth2/v2.0/token` with the application/x-www-form-urlencoded variables of `grant_type=authorization_code`, `client_id=`, `code=`, `redirect_url=https://`, and `client_secret=`... – SnoopDougg Dec 21 '18 at 15:58

0 Answers0