How to verify JWT from AWS Cognito in the API backend?

Execute an Authorization Code Grant Flow

Assuming that you:

  • have correctly configured a user pool in AWS Cognito, and
  • are able to signup/login and get an access code via:

    https://<your-domain>.auth.us-west-2.amazoncognito.com/login?response_type=code&client_id=<your-client-id>&redirect_uri=<your-redirect-uri>
    

Your browser should redirect to <your-redirect-uri>?code=4dd94e4f-3323-471e-af0f-dc52a8fe98a0


Now you need to pass that code to your back-end and have it request a token for you.

POST https://<your-domain>.auth.us-west-2.amazoncognito.com/oauth2/token

  • set your Authorization header to Basic and use username=<app client id> and password=<app client secret> per your app client configured in AWS Cognito
  • set the following in your request body:
    • grant_type=authorization_code
    • code=<your-code>
    • client_id=<your-client-id>
    • redirect_uri=<your-redirect-uri>

If successful, your back-end should receive a set of base64 encoded tokens.

{
    id_token: '...',
    access_token: '...',
    refresh_token: '...',
    expires_in: 3600,
    token_type: 'Bearer'
}

Now, according to the documentation, your back-end should validate the JWT signature by:

  1. Decoding the ID token
  2. Comparing the local key ID (kid) to the public kid
  3. Using the public key to verify the signature using your JWT library.

Since AWS Cognito generates two pairs of RSA cryptograpic keys for each user pool, you need to figure out which key was used to encrypt the token.

Here's a NodeJS snippet that demonstrates verifying a JWT.

import jsonwebtoken from 'jsonwebtoken'
import jwkToPem from 'jwk-to-pem'

const jsonWebKeys = [  // from https://cognito-idp.us-west-2.amazonaws.com/<UserPoolId>/.well-known/jwks.json
    {
        "alg": "RS256",
        "e": "AQAB",
        "kid": "ABCDEFGHIJKLMNOPabc/1A2B3CZ5x6y7MA56Cy+6ubf=",
        "kty": "RSA",
        "n": "...",
        "use": "sig"
    },
    {
        "alg": "RS256",
        "e": "AQAB",
        "kid": "XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=",
        "kty": "RSA",
        "n": "...",
        "use": "sig"
    }
]

function validateToken(token) {
    const header = decodeTokenHeader(token);  // {"kid":"XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=", "alg": "RS256"}
    const jsonWebKey = getJsonWebKeyWithKID(header.kid);
    verifyJsonWebTokenSignature(token, jsonWebKey, (err, decodedToken) => {
        if (err) {
            console.error(err);
        } else {
            console.log(decodedToken);
        }
    })
}

function decodeTokenHeader(token) {
    const [headerEncoded] = token.split('.');
    const buff = new Buffer(headerEncoded, 'base64');
    const text = buff.toString('ascii');
    return JSON.parse(text);
}

function getJsonWebKeyWithKID(kid) {
    for (let jwk of jsonWebKeys) {
        if (jwk.kid === kid) {
            return jwk;
        }
    }
    return null
}

function verifyJsonWebTokenSignature(token, jsonWebKey, clbk) {
    const pem = jwkToPem(jsonWebKey);
    jsonwebtoken.verify(token, pem, {algorithms: ['RS256']}, (err, decodedToken) => clbk(err, decodedToken))
}


validateToken('xxxxxxxxx.XXXXXXXX.xxxxxxxx')

Turns out I didn't read the docs right. It's explained here (scroll down to "Using ID Tokens and Access Tokens in your Web APIs").

The API service can download Cognito's secrets and use them to verify received JWT's. Perfect.

Edit

@Groady's comment is on point: but how do you validate the tokens? I'd say use a battle-tested library like jose4j or nimbus (both Java) for that and don't implement the verification from scratch yourself.

Here's an example implementation for Spring Boot using nimbus that got me started when I recently had to implement this in java/dropwizard service.


Short answer:
You can get the public key for your user pool from the following endpoint:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
If you successfully decode the token using this public key then the token is valid else it is forged.


Long answer:
After you successfully authenticate via cognito, you get your access and id tokens. Now you want to validate whether this token has been tampered with or not. Traditionally we would send these tokens back to the authentication service (which issued this token at the first place) to check if the token is valid. These systems use symmetric key encryption algorithms such as HMAC to encrypt the payload using a secret key and so only this system is capable to tell if this token is valid or not.
Traditional auth JWT token Header:

{
   "alg": "HS256",
   "typ": "JWT"
}

Note here that encryption algorithm used here is symmetric - HMAC + SHA256

But modern authentication systems like Cognito use asymmetric key encryption algorithms such as RSA to encrypt the payload using a pair of public and private key. Payload is encrypted using a private key but can be decoded via public key. Major advantage of using such an algorithm is that we don't have to request a single authentication service to tell if a token is valid or not. Since everyone has access to the public key, anyone can verify validity of token. The load for validation is fairly distributed and there is no single point of failure.
Cognito JWT token header:

{
  "kid": "abcdefghijklmnopqrsexample=",
  "alg": "RS256"
}

Asymmetric encryption algorithm used in this case - RSA + SHA256


Here's a way to verify the signature on NodeJS:

var jwt = require('jsonwebtoken');
var jwkToPem = require('jwk-to-pem');
var pem = jwkToPem(jwk);
jwt.verify(token, pem, function(err, decoded) {
  console.log(decoded)
});


// Note : You can get jwk from https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json