Validating bearer JWT access tokens

OAuth 2.0 protected resources (web APIs) need to validate each submitted access token, and these can be implemented as signed JSON Web Tokens (JWT). The core OAuth 2.0 spec leaves the encoding and processing of access and refresh tokens up to implementers.

The Connect2id authorisation server, for example, issues by default access tokens that are represented by RSA-signed JWTs. These can be validated quickly and efficiently, with a copy of the public RSA key for the private RSA key was used to sign the JWT. Cryptographic validation is typically much faster than having identifier-based access tokens, where the resource server must make a network query to the OAuth 2.0 server to get the underlying authorisation information.

The Connect2id server publishes its public keys at a discoverable URL as JSON Web Key (JWK) set document. This document can be cached by the resource server to speed up key lookups.

JWT validation framework

The Nimbus JOSE+JWT library provides a secure framework that takes care of all necessary steps to validate a JWT:

  1. JWT parsing -- The access token string is parsed as a JWT. If parsing fails the token is considered invalid and the request must be denied.
  2. Algorithm check -- The JWS algorithm specified in the JWT header is checked whether it matches the agreed / expected one (e.g. RS256 for RSA PKCS #1 signature with SHA-256). If a token with an unexpected algorithm is received it is rejected. This measure prevents downgrade attacks and other attacks that may become possible if tokens with any JOSE algorithm are generally accepted.
  3. Signature check -- The digital signature is verified by trying one or more selected public RSA keys (obtained from the authorisation server at a public URL).
  4. JWT claims check -- The JWT claims set is validated, e.g. to ensure the token has not expired and matches the expected issuer and audience.

Example Java code to set up a JWT validator which obtains the necessary public RSA keys from a JSON document published by the OAuth 2.0 server (requires Nimbus JOSE+JWT v4.14+):

import com.nimbusds.jose..*;
import com.nimbusds.jose.jwk.source.*;
import com.nimbusds.jwt.*;
import com.nimbusds.jwt.proc.*;

// The access token to validate, typically submitted with a HTTP header like
// Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InMxIn0.eyJzY3A...
String accessToken =
    "eyJhbGciOiJSUzI1NiIsImtpZCI6InMxIn0.eyJzY3AiOlsib3BlbmlkIiwiZW1haWwiLCJwcm9maWxl" +
    "Il0sImV4cCI6MTQ2MDM0NTczNiwic3ViIjoiYWxpY2UiLCJpc3MiOiJodHRwczpcL1wvZGVtby5jMmlk" +
    "LmNvbVwvYzJpZCIsInVpcCI6eyJncm91cHMiOlsiYWRtaW4iLCJhdWRpdCJdfSwiY2xtIjpbIiE1djhI" +
    "Il0sImNpZCI6IjAwMDEyMyJ9.Xeg3cMrePht8R0731mfndUDoX48NWhfCuEjcEERcZ3krfnOacNJzyJd" +
    "7zOWdNrlvEpJMjmmgkbhZOMJlVMv4fQnGB2d3eevmtjuT7hMnJVQc_4h80ODHPMlW27T0Iukpe7Y-A-R" +
    "rROP5yinry7BFBL2nVWrNtB9IS11H9C8X5fQ";

// Set up a JWT processor to parse the tokens and then check their signature
// and validity time window (bounded by the "iat", "nbf" and "exp" claims)
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();

// The public RSA keys to validate the signatures will be sourced from the
// OAuth 2.0 server's JWK set, published at a well-known URL. The RemoteJWKSet
// object caches the retrieved keys to speed up subsequent look-ups and can
// also gracefully handle key-rollover
JWKSource keySource = new RemoteJWKSet(new URL("https://demo.c2id.com/c2id/jwks.json"));

// The expected JWS algorithm of the access tokens (agreed out-of-band)
JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;

// Configure the JWT processor with a key selector to feed matching public
// RSA keys sourced from the JWK set URL
JWSKeySelector keySelector = new JWSVerificationKeySelector(expectedJWSAlg, keySource);
jwtProcessor.setJWSKeySelector(keySelector);

// Process the token
SecurityContext ctx = null; // optional context parameter, not required here
JWTClaimsSet claimsSet = jwtProcessor.process(accessToken, ctx);

// Print out the token claims set
System.out.println(claimsSet.toJSONObject());

The resulting access token claims set:

{
  "sub" : "alice",
  "cid" : "000123",
  "iss" : "https:\/\/demo.c2id.com\/c2id",
  "exp" : 1460345736,
  "scp" : ["openid","email","profile"],
  "clm" : ["!5v8H"],
  "uip" : {"groups":["admin","audit"]}
}

Encrypted tokens

The JWT processing framework can also handle tokens that are encrypted after signing (or just encrypted). For that the JWT processor must be configured with an appropriate selector for the JWE decryption keys.

Example setup to handle tokens that are directly encrypted with a shared AES key:

// The AES key, obtained from a Java keystore, etc.
SecretKey secretKey = null;

// The expected JWE algorithm and method
JWEAlgorithm expectedJWEAlg = JWEAlgorithm.DIR;
EncryptionMethod expectedJWEEnc = EncryptionMethod.A128GCM;

// The JWE key source
JWKSource jweKeySource = new ImmutableSecret(secretKey);

// Configure a key selector to handle the decryption phase
JWEKeySelector jweKeySelector = new JWEDecryptionKeySelector(expectedJWEAlg, expectedJWEEnc, jweKeySource);
jwtProcessor.setJWEKeySelector(jweKeySelector);

Plugging alternative key sources

The example above used a JWK set URL to feed the key selector. Other types of key sources that are supported of the box:

  • ImmutableJWKSet -- to specify a set of JWK candidates directly by value.

    import java.io.File;
    import com.nimbusds.jose.jwk.*;
    import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
    
    // Load JWK set from JSON file, etc.
    JWKSet jwkSet = JWKSet.load(new File("/var/server/jwkset.json");
    
    // Create JWK source backed by a JWK set
    JWKSource keySource = new ImmutableJWKSet(jwkSet);
    
  • ImmutableSecret -- to specify a singleton JWK that is symmetric key; can be used for HMAC verification, direct AES decryption, password-based decryption and other cases that require a secret key.

    import java.security.SecureRandom;
    import com.nimbusds.jose.jwk.source.ImmutableSecret;
    
    // Generate secret
    byte[] secret = new byte[32];
    new SecureRandom().nextBytes(secret);
    
    // Create JWK source backed by a singleton secret key
    JWKSource keySource = new ImmutableSecret(secret);
    

You can implement your own custom JWK source, for example based on a Java keystore or some database.

Plugging in your own claims validator

The default JWT claims check (at step 4) only examines the "exp" and "nbf" claims that set the token validity window. You can implement other checks if needed, for example to ensure the token was issued by an accepted OAuth 2.0 server:

jwtProcessor.setJWTClaimsVerifier(new DefaultJWTClaimsVerifier() {      
    @Override
    public void verify(JWTClaimsSet claimsSet) 
        throws BadJWTException {

        super.verify(claimsSet);

        // If present the actual expiration timestamp is checked by the 
        // overridden method
        if (claimsSet.getExpirationTime() == null) {
            throw new BadJWTException("Missing token expiration claim");
        }

        if (! "https://demo.c2id.com/c2id".equals(claimsSet.getIssuer()) {
            throw new BadJWTException("Token issuer not accepted");
        }
    }
});