109

I'm building a system consisting of an Angular2 single page app and a REST API running on ECS. The API runs on .Net/Nancy, but that might well change.

I would like to give Cognito a try and this is how I imagined the authentication workflow:

  1. SPA signs in user and receives a JWT
  2. SPA sends JWT to REST API with every request
  3. REST API verfies that the JWT is authentic

My question is about step 3. How can my server (or rather: my stateless, auto-scaled, load-balanced Docker containers) verify that the token is authentic? Since the "server" hasn't issued the JWT itself, it can't use its own secret (as described in the basic JWT example here).

I have read through the Cognito docs and googled a lot, but I can't find any good guideline about what to do with the JWT on the server side.

EagleBeak
  • 6,939
  • 8
  • 31
  • 47
  • 2
    If you're using a Node/Express app, I've created an npm package called [cognito-express](https://www.npmjs.com/package/cognito-express) which pretty much does what you're looking to do - downloads the JWKs from your Cognito User Pool and verifies the signature of JWT of ID Token or Access Token. – ghdna Jul 05 '17 at 17:56
  • @ghdna I've recently downloaded cognito-express and installed it on my server but from Cognito on my client side I only get accessKey, secretKey, sessionKey and expiration. I can't find ID Token or Access Token being returned from anywhere. There's a Refresh Token somewhere out there too. So all I'm getting in my console from cogito-express at the moment is Access Token missing from header or Not a valid JWT. Any pointers? – elarcoiris Nov 19 '17 at 22:48
  • I hope you could have given a clear code sample for JWT validation, as per a aws quickstart project the JWT is decoded (base64 conversion) to get the "kid" then fetch the JWK from the url, convert to PEM and then validate. I am stuck in PEM conversion. – Abdeali Chandanwala Oct 09 '20 at 06:19

11 Answers11

53

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.

EagleBeak
  • 6,939
  • 8
  • 31
  • 47
  • 88
    The documentation is crap at best. Step 6 says _"Verify the signature of the decoded JWT token"_... yeah... HOW!?!? According to this [this blog post](https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/) you need to convert the JWK to a PEM. Could they not put how to do this on the official docs?! – Will Apr 09 '17 at 07:00
  • A followup to Groady as I'm going through this. Depending on your library, you shouldn't need to convert to pem. For example, I'm on Elixir, and Joken takes the RSA key map exactly as provided by Amazon. I spent a lot of time spinning my wheels when I thought the key had to be a string. – Law Jul 12 '17 at 18:37
  • Thanks for the example link! Helped a lot to understand how to use the nimbus library. Any ideas, however, if I can extract the remote JWK set to be an external cache? I'd like to put the JWKSet in Elasticache instead. – Eric B. Mar 13 '18 at 21:26
48

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')
andrewgazelka
  • 1,746
  • 1
  • 16
  • 26
Derek Soike
  • 11,238
  • 3
  • 79
  • 74
  • Is `` the same as ``? – Zach Saucier Oct 17 '20 at 21:35
  • Answering my question above: It is but it's not necessary in the body if you are providing a secret in the header. – Zach Saucier Oct 17 '20 at 23:23
  • 1
    `new Buffer(headerEncoded, 'base64')` should now be `Buffer.from(headerEncoded, 'base64')` – Zach Saucier Oct 18 '20 at 03:48
  • This is a fantastic answer that saved me a lot of time! I created a working sample that demonstrates the full flow using the token verifier package below. https://gitlab.com/danderson00/cognito-srp-js – Dale Anderson Jul 01 '21 at 23:52
  • Thank you for this response, it clarifies a lot. However, this answer probably lacks a comment that it is not the only way the flow can be implemented. For example, if you are using Amplify library, then you are going to get all the tokens from Cognito on front-end side. I wonder what will be the best practice in this case? What comes to my mind is that we can still pass the `acess_token` received on FE to the BE side, where the BE can validate it and provide the FE with its own token to use the API. What are everyone's thoughts on this? – Server Khalilov Aug 18 '22 at 10:09
47

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 
FacePalm
  • 10,992
  • 5
  • 48
  • 50
  • Thanks ,saved my day! – Nirojan Selvanathan Jul 24 '18 at 07:08
  • 2
    Thanks for this! There were also a bunch of details that I needed to consider when converting the JWK to a PEM: https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/ – redgeoff Jan 22 '19 at 01:04
  • 1
    Should we save the content of JWKs in local configuration for re-use? Is this content expire or become invalid in the future? – Nghia Jun 12 '19 at 04:19
  • @Nghia "Instead of downloading the JWK Set directly from your Lambda function, you can download it manually once, converting the keys to PEMs and uploading them with your Lambda function." from https://aws.amazon.com/blogs/mobile/integrating-amazon-cognito-user-pools-with-api-gateway/ – dubucha Jun 13 '20 at 22:52
  • 1
    A problem that we have identified recently, is that a "valid token" isn't necessarily a valid token. Imagine if you revoke a token. The JWT will still be a valid token. However the token is not valid to use with the service. Just checking the token's validity itself does not help you know whether you can use it or not with AWS Cognito – Christopher Thomas Mar 21 '22 at 15:40
17

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

Gautam Jain
  • 651
  • 8
  • 16
17

AWS released a JavaScript library specifically for this purpose: https://github.com/awslabs/aws-jwt-verify.

The library has similar machinery to other libraries out there and mentioned here, such as automatically downloading, and caching, the JWKS (the public keys with which Cognito JWTs can be verified). It's written in pure TypeScript and has 0 dependencies.

import { CognitoJwtVerifier } from "aws-jwt-verify";

// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
  userPoolId: "<user_pool_id>",
  tokenUse: "access",
  clientId: "<client_id>",
});

try {
  const payload = await verifier.verify(
    "eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
  );
  console.log("Token is valid. Payload:", payload);
} catch {
  console.log("Token not valid!");
}

(By the way, the library also includes a class that works for other identity providers than Cognito)

Disclaimer: I'm one of the authors of the library. We're looking forward to customer feedback––do leave us a GitHub issue.

Otto
  • 1,787
  • 1
  • 17
  • 25
14

cognito-jwt-verifier is a tiny npm package to verify ID and access JWT tokens obtained from AWS Cognito in your node/Lambda backend with minimal dependencies.

Disclaimer: I'm the author of this. I came up with it because I couldn't find anything checking all the boxes for me:

  • minimal dependencies
  • framework agnostic
  • JWKS (public keys) caching
  • test coverage

Usage (see github repo for a more detailed example):

const { verifierFactory } = require('@southlane/cognito-jwt-verifier')
 
const verifier = verifierFactory({
  region: 'us-east-1',
  userPoolId: 'us-east-1_PDsy6i0Bf',
  appClientId: '5ra91i9p4trq42m2vnjs0pv06q',
  tokenType: 'id', // either "access" or "id"
})

const token = 'eyJraWQiOiI0UFFoK0JaVE...' // clipped 
 
try {
  const tokenPayload = await verifier.verify(token)
} catch (e) {
  // catch error and act accordingly, e.g. throw HTTP 401 error
}
Max Ivanov
  • 5,695
  • 38
  • 52
10

I had a similar problem but without using the API Gateway. In my case I wanted to verify the signature of a JWT token obtained via the AWS Cognito Developer Authenticated identity route.

Like many posters on various sites I had trouble piecing together exactly the bits I needs to verify the signature of an AWS JWT token externally i.e., server side or via script

I think I figured out out and put a gist to verify an AWS JWT token signature. It'll verify an AWS JWT/JWS token with either pyjwt or PKCS1_v1_5c from Crypto.Signature in PyCrypto

So, yes this was python in my case but it's also doable easily in node (npm install jsonwebtoken jwk-to-pem request).

I attempted to highlight some gotchas in the comments because when I was trying to figure this out I was mostly doing the right thing but there were some nuances like python dict ordering, or lack there of, and json representation.

Hopefully it may help somebody somewhere.

David Kierans
  • 1,599
  • 1
  • 16
  • 24
2

this is working for me in dot net 4.5

    public static bool VerifyCognitoJwt(string accessToken)
    {
        string[] parts = accessToken.Split('.');

        string header = parts[0];
        string payload = parts[1];

        string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
        JObject headerData = JObject.Parse(headerJson);

        string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
        JObject payloadData = JObject.Parse(payloadJson);

        var kid = headerData["kid"];
        var iss = payloadData["iss"];

        var issUrl = iss + "/.well-known/jwks.json";
        var keysJson= string.Empty;

        using (WebClient wc = new WebClient())
        {
            keysJson = wc.DownloadString(issUrl);
        }

        var keyData = GetKeyData(keysJson,kid.ToString());

        if (keyData==null)
            throw new ApplicationException(string.Format("Invalid signature"));

        var modulus = Base64UrlDecode(keyData.Modulus);
        var exponent = Base64UrlDecode(keyData.Exponent);

        RSACryptoServiceProvider provider = new RSACryptoServiceProvider();

        var rsaParameters= new RSAParameters();
        rsaParameters.Modulus = new BigInteger(modulus).ToByteArrayUnsigned();
        rsaParameters.Exponent = new BigInteger(exponent).ToByteArrayUnsigned();

        provider.ImportParameters(rsaParameters);

        SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
        byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));

        RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(provider);
        rsaDeformatter.SetHashAlgorithm(sha256.GetType().FullName);

        if (!rsaDeformatter.VerifySignature(hash, Base64UrlDecode(parts[2])))
            throw new ApplicationException(string.Format("Invalid signature"));

        return true;
    }

 public class KeyData
    {
        public string Modulus { get; set; }
        public string Exponent { get; set; }
    }

    private static KeyData GetKeyData(string keys,string kid)
    {
        var keyData = new KeyData();

        dynamic obj = JObject.Parse(keys);
        var results = obj.keys;
        bool found = false;

        foreach (var key in results)
        {
            if (found)
                break;

            if (key.kid == kid)
            {
                keyData.Modulus = key.n;
                keyData.Exponent = key.e;
                found = true;
            }
        }

        return keyData;
    }
Arvind Krmar
  • 2,154
  • 2
  • 13
  • 18
2

You can get insights from the Lambda code here

https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html

https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt

In Golang https://gist.github.com/tmaiaroto/e2ee5e88fc6ae035307d7c5ee71a99cf

Montaro
  • 9,240
  • 6
  • 29
  • 30
  • Awslabs is a good resource even though example implementation is for Lambda. They use `python-jose` to decode and verify JWT. – Jernej Jerin May 03 '20 at 09:29
1

This is based on the elaborate explanation from Derek (answer). I have been able to create a working sample for PHP.

I have used https://github.com/firebase/php-jwt for pem creation and code verification.

This code is used after you received a set of base64 encoded tokens.

<?php

require_once(__DIR__ . '/vendor/autoload.php');

use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;

function debugmsg($msg, $output) {
    print_r($msg . "\n");
}

$tokensReceived = array(
    'id_token' => '...',
    'access_token' => '...',
    'refresh_token' => '...',
    'expires_in' => 3600,
    'token_type' => 'Bearer'
);

$idToken = $tokensReceived['id_token'];

// 'https://cognito-idp.us-west-2.amazonaws.com/<pool-id>/.well-known/jwks.json'
$keys = json_decode('<json string received from jwks.json>');

$idTokenHeader = json_decode(base64_decode(explode('.', $idToken)[0]), true);
print_r($idTokenHeader);

$remoteKey = null;

$keySets = JWK::parseKeySet($keys);

$remoteKey = $keySets[$idTokenHeader['kid']];

try {
    print_r("result: ");
    $decoded = JWT::decode($idToken, $remoteKey, array($idTokenHeader['alg']));
    print_r($decoded);
} catch(Firebase\JWT\ExpiredException $e) {
    debugmsg("ExpiredException","cognito");
} catch(Firebase\JWT\SignatureInvalidException $e) {
    debugmsg("SignatureInvalidException","cognito");
} catch(Firebase\JWT\BeforeValidException $e) {
    debugmsg("BeforeValidException","cognito");
}

?>
1

Someone also wrote a python package called cognitojwt that works in both async/sync mode to decode and verify Amazon Cognito JWT.

Jernej Jerin
  • 3,179
  • 9
  • 37
  • 53