Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does AWS Cognito use multiple public keys for JWTs?

When I download the JWT set for a particular User Pool available at: https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json

The JSON contains 2 keys. All the users that I have created for the pool seem to use just one of these keys.

What is the reason for having multiple keys per User Pool?

like image 480
Jackalope Avatar asked May 15 '17 11:05

Jackalope


People also ask

Is Cognito Multi AZ?

In each Region, Amazon Cognito is distributed across multiple Availability Zones. These Availability Zones are physically isolated from each other, but are united by private, low-latency, high-throughput, and highly redundant network connections.

How does AWS Cognito hash passwords?

Cognito Identity does not receive or store user credentials. Cognito Identity uses the token from the identity provider to obtain a unique identifier for the user and then hashes it using a one-way hash so that the same user can be recognized again in the future without storing the actual user identifier.

Does Cognito use JWT?

After a user logs in, an Amazon Cognito user pool returns a JWT. The JWT is a base64url-encoded JSON string ("claims") that contains information about the user. Amazon Cognito returns three tokens: the ID token, the access token, and the refresh token.

What is the difference between access token and ID token in AWS Cognito?

The ID token contains claims about the identity of the authenticated user, such as name and email. The access token contains claims about the authenticated user, a list of the user's groups, and a list of scopes. Amazon Cognito also has tokens that you can use to get new tokens or revoke existing tokens.


1 Answers

According to the documentation:

Amazon Cognito generates two pairs of RSA cryptograpic keys for each user pool. One of the private keys is used to sign the token.

Presumably this is for security reasons. From limited trial-and-error it appears that one is used to encrypt id tokens and the other is used to encrypt access tokens. Or perhaps the purpose is to support key rotation (as suggested by @Michael-sqlbot).


Once you understand the why (or don't understand), the question becomes what to do with the two keys.

Again referring to the documentation, to validate the JWT signature you need to:

  1. Decode the ID token
  2. Compare the local key ID (kid) to the public kid
  3. Use the public key to verify the signature using your JWT library.

Steps 1 and 2 are necessary to figure out which RSA cryptograpic key was used to encrypt your 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, function(err, decodedToken) {
        if (err) {
            console.error(err)
        } else {
            console.log(decodedToken)
        }
    })
}

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

func 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'] }, function(err, decodedToken) {
        return clbk(err, decodedToken)
    })
}

validateToken('xxxxxxxxx.XXXXXXXX.xxxxxxxx')
like image 109
Derek Soike Avatar answered Oct 19 '22 15:10

Derek Soike