Files
graphql-api-js/src/cognito/cognito-client.ts
T

167 lines
4.2 KiB
TypeScript

import { promisify } from 'util';
import * as Axios from 'axios';
import * as jsonwebtoken from 'jsonwebtoken';
import { Maybe } from '../types/types';
const jwkToPem = require('jwk-to-pem');
export interface ClaimVerifyRequest {
readonly token?: string;
}
export interface ClaimVerifyResult {
readonly userName: string;
readonly isValid: boolean;
readonly error?: string;
readonly accessToken: Maybe<AccessTokenClaim>;
readonly idToken: Maybe<IdTokenClaim>;
}
interface TokenHeader {
kid: string;
alg: string;
}
interface PublicKey {
alg: string;
e: string;
kid: string;
kty: string;
n: string;
use: string;
}
interface PublicKeyMeta {
instance: PublicKey;
pem: string;
}
interface PublicKeys {
keys: PublicKey[];
}
interface MapOfKidToPublicKey {
[key: string]: PublicKeyMeta;
}
interface Claim {
token_use?: string;
auth_time: number;
iat: number;
exp: number;
iss: string;
}
interface AccessTokenClaim extends Claim {
token_use: string;
username: string;
client_id: string;
'cognito:groups': string;
scope: string;
}
interface IdTokenClaim extends Claim {
'custom:adgroups': string; // 'custom:adgroups': '[Domain Users, Images_Write, VPN_Users, WebAdmin, WebAdminSuperUser]',
'cognito:username': string;
token_use: string;
email_verified: boolean;
nonce: string;
at_hash: string;
email: string;
aud: string;
sub: string;
identities: [Identity];
}
interface Identity {
userId: string;
providerName: string;
providerType: string;
issuer: string;
primary: string;
dateCreated: string;
}
const cognitoPoolId = process.env.COGNITO_POOL_ID || '';
if (!cognitoPoolId) {
throw new Error('env var required for cognito pool');
}
const cognitoIssuer = `https://cognito-idp.eu-west-1.amazonaws.com/${cognitoPoolId}`;
let cacheKeys: MapOfKidToPublicKey | undefined;
const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
if (!cacheKeys) {
const url = `${cognitoIssuer}/.well-known/jwks.json`;
const publicKeys = await Axios.default.get<PublicKeys>(url);
cacheKeys = publicKeys.data.keys.reduce((agg, current) => {
const pem = jwkToPem(current);
agg[current.kid] = { instance: current, pem };
return agg;
}, {} as MapOfKidToPublicKey);
return cacheKeys;
} else {
return cacheKeys;
}
};
const verifyPromised = promisify(jsonwebtoken.verify.bind(jsonwebtoken));
const verifyToken = async (
request: ClaimVerifyRequest,
): Promise<ClaimVerifyResult> => {
try {
const token = request.token;
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.pem)) 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 !== 'access' && claim.token_use !== 'id') {
throw new Error('invalid token type');
}
if (claim.token_use === 'access') {
// Access token, check for scopes
const accessClaim = claim as AccessTokenClaim;
const who = accessClaim.username ?? accessClaim.client_id;
console.debug(`accessClaim confirmed for ${who}`);
return {
userName: who,
isValid: true,
idToken: null,
accessToken: accessClaim,
};
} else {
const idClaim = claim as IdTokenClaim;
console.debug(`idClaim confirmed for ${idClaim.email}`);
return {
userName: idClaim.email,
isValid: true,
idToken: idClaim,
accessToken: null,
};
}
} catch (error) {
return {
userName: '',
error: error.message,
isValid: false,
idToken: null,
accessToken: null,
};
}
};
export { verifyToken };