Created
May 27, 2021 20:39
-
-
Save billjohnston/e96f7165f50f8194de0c45ccc3a9f1b8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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