Skip to content

Instantly share code, notes, and snippets.

@billjohnston
Created May 27, 2021 20:39
Show Gist options
  • Save billjohnston/e96f7165f50f8194de0c45ccc3a9f1b8 to your computer and use it in GitHub Desktop.
Save billjohnston/e96f7165f50f8194de0c45ccc3a9f1b8 to your computer and use it in GitHub Desktop.
import * as jsonwebtoken from 'jsonwebtoken'
import jwkToPem from 'jwk-to-pem'
import nodeAjax from 'lambdas/util/nodeAjax'
import { HttpMethod } from 'types'
import { Middleware } from 'lambda-api'
import {
cognitoUserPoolId,
cognitoUserPoolClientId,
region,
} from 'lambdas/functions/restApi/envVars'
export interface ClaimVerifyRequest {
readonly token?: string
}
export interface ClaimVerifyResult {
readonly sub: string
readonly groups: string[]
readonly email: string
}
interface TokenHeader {
kid: string
alg: string
}
interface PublicKey {
alg: string
e: string
kid: string
kty: 'RSA'
n: string
use: string
}
interface PublicKeys {
keys: PublicKey[]
}
interface MapOfKidToPublicKey {
[key: string]: string
}
interface Claim {
/* eslint-disable camelcase */
token_use: 'id' | 'access'
auth_time: number
/* eslint-enable */
iss: string
exp: number
email: string
sub: string
aud: string
'cognito:groups': string[]
}
const cognitoIssuer =
let cacheKeys: MapOfKidToPublicKey | undefined
const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
const url =
const { keys } = await nodeAjax<PublicKeys>({
url,
method: HttpMethod.GET,
})
cacheKeys = keys.reduce((agg, current) => {
const pem = jwkToPem(current)
return {
...agg,
[current.kid]: pem,
}
}, {} as MapOfKidToPublicKey)
return cacheKeys
}
return cacheKeys
}
const verifyPromised = (token: string, pubKey: string) =>
new Promise((resolve, reject) => {
jsonwebtoken.verify(
token,
pubKey,
{ algorithms: ['RS256'] },
(err, decoded) => {
if (err) {
reject(err)
} else {
resolve(decoded)
}
}
)
})
export const verifyToken = async (
token: string
): Promise<ClaimVerifyResult> => {
const tokenSections = (token || '').split('.')
if (tokenSections.length < 2) {
throw new Error('Requested token is invalid')
}
const headerJSON = Buffer.from(tokenSections[0], 'base64').toString('utf8')
const header = JSON.parse(headerJSON) as TokenHeader
const keys = await getPublicKeys()
const key = keys[header.kid]
if (key === undefined) {
throw new Error('Claim made for unknown kid')
}
const claim = (await verifyPromised(token, key)) as Claim
const currentSeconds = Math.floor(new Date().valueOf() / 1000)
if (currentSeconds > claim.exp || currentSeconds < claim.auth_time) {
throw new Error('Claim is expired or invalid')
}
if (claim.iss !== cognitoIssuer) {
throw new Error('Claim issuer is invalid')
}
if (claim.token_use !== 'id') {
throw new Error('Claim use is not id')
}
if (claim.aud !== cognitoUserPoolClientId) {
throw new Error('Claim was not issued for this audience')
}
return {
sub: claim.sub,
email: claim.email,
groups: claim['cognito:groups'],
}
}
const cognitoAuthMiddleware = (allowedGroups: string[]): Middleware => async (
req,
res,
next
) => {
const jwt = req.headers.authorization || ''
res.error(403, 'Not authorized', 'No Token')
}
try {
const token = jwt.replace('JWT ', '')
const { sub, email, groups } = await verifyToken(token)
if (allowedGroups) {
const isMemberOfGroups = allowedGroups.every(
(group: string) => groups.indexOf(group) !== -1
)
res.error(403, 'Not authorized', 'Not in group')
}
}
req.user = {
id: sub,
email,
groups,
}
next()
} catch (e) {
res.error(403, 'Not authorized', e.message)
}
}
export default cognitoAuthMiddleware
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment