jwtamazon-cognitoverifyjwkjose

how to decode and verify aws cognito JWT IdToken from nodeJS and Browser?


I would like to decode & verify the IdToken provided by AWS cognito. Simple code that could be used on NodeJs(server) and Browser (the same code).

I tried to use the classic jwt-decode but it has some problems on the browser side due dependencies on crypto lib.


Solution

  • I end up using Jose only, as I mentioned in a comment. I faced some minor issues. Jose offers a built-int method to get JWK, but I wanted to cache the response in-memory by my own means. USER_POOL_ID and REGION are required as env.vars.

    I am looking forward to any suggestions.

    import axios from 'axios'
    import * as jose from 'jose'
    import { isNumber } from 'lodash'
    
    export const authCookies = {
        userCookie: 'auth.idToken',
        accessTokenCookie: 'auth._token.local',
        strategy: 'auth.strategy',
        expiration: 'auth._token_expiration.local'
    }
    
    const USER_POOL_ID = process.env.USER_POOL_ID
    const REGION = process.env.AWS_REGION || 'us-east-1'
    
    const cachedJwks = []
    
    /**
     * @description get cognito JWKS and cache the result. If kid is no found refresh cache.
     * @param {string} currentKid - current kid to double check cache or refresh.
     */
    const getJwk = async currentKid => {
        if (currentKid) {
            // console.log('-----> cachedJwks', cachedJwks)
            const cachedKey = cachedJwks.find(item => item.kid === currentKid)
            if (cachedKey) {
                // console.log('-----> cachedKey', cachedKey)
                return cachedKey
            }
            cachedJwks.length = 0
    
            const { data } = await axios.get(`https://cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}/.well-known/jwks.json`)
                .catch(error => {
                    console.error('Error on getJwk axios call', error)
                    return { data: null }
                })
            if (data?.keys) {
                // console.log('-----> data call')
                const key = data.keys.find(item => item.kid === currentKid)
                if (key) {
                    cachedJwks.push(...data.keys)
                    return key
                }
            }
        }
        console.error('No Key found to verify the token: ', currentKid)
        return null
    }
    
    /**
     * @description check expiration date of token.
     * @param {number} expiration - expiration date in seconds.
     */
    export const isExpired = expiration => {
        const currentTime = Math.floor(Date.now() / 1000)
        console.log(' expiration >= currentTime', expiration, currentTime)
        return !isNumber(expiration) || expiration < currentTime
    }
    
    /**
     * @description verify token with congnito.
     * @param {string} token - token to verify.
     */
    export const verifyToken = async token => {
        // @todo: create singletone on lambda pattern
        try {
            // get header using jose get protected header method.
            const header = jose.decodeProtectedHeader(token)
    
            const jwk = await getJwk(header.kid)
    
            if (jwk) {
                const pem = await jose.importJWK(jwk, 'RS256')
                const { payload: verifiedBufferData } = await jose.compactVerify(token, pem)
                const verifiedData = JSON.parse(Buffer.from(verifiedBufferData).toString('utf8'))
    
                if (isExpired(verifiedData.exp)) {
                    console.error('Token expired')
                    return null
                }
    
                return verifiedData
            }
        } catch (error) {
            console.error('Error on verifyToken', error)
        }
        return null
    }