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.
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
}