From 4b7133a9ab9ddfbf86c283662643c46817fd63d2 Mon Sep 17 00:00:00 2001 From: Niklas Fondberg Date: Tue, 6 Jul 2021 10:31:04 +0200 Subject: [PATCH] Add token verification with development basic header --- src/cognito/cognito-client.ts | 166 ++++++++++++++++++++++++++++++++++ src/index.ts | 13 ++- 2 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/cognito/cognito-client.ts diff --git a/src/cognito/cognito-client.ts b/src/cognito/cognito-client.ts new file mode 100644 index 0000000..22c053d --- /dev/null +++ b/src/cognito/cognito-client.ts @@ -0,0 +1,166 @@ +import { promisify } from 'util'; +import * as Axios from 'axios'; +import * as jsonwebtoken from 'jsonwebtoken'; +import { Maybe } from '../datasources/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; + readonly idToken: Maybe; +} + +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 => { + if (!cacheKeys) { + const url = `${cognitoIssuer}/.well-known/jwks.json`; + const publicKeys = await Axios.default.get(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 => { + try { + // console.log(`user claim verify invoked for ${JSON.stringify(request)}`); + 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; + console.debug(`accessClaim confirmed for ${accessClaim.username}`); + return { + userName: accessClaim.username, + 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 }; diff --git a/src/index.ts b/src/index.ts index c0778bf..cc97504 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import dotenv from 'dotenv'; dotenv.config(); -import { ApolloServer } from 'apollo-server'; +import { ApolloServer, AuthenticationError } from 'apollo-server'; import knexStringcase from 'knex-stringcase'; import { dbConfig } from './config'; import { typeDefs } from './schema'; @@ -10,6 +10,7 @@ import { OrderAPI } from './datasources/order-api'; import { ProductAPI } from './datasources/product-api'; import { DesignerAPI } from './datasources/designer-api'; import { Api2DataSource } from './datasources/api2-datasource'; +import { verifyToken } from './cognito/cognito-client'; console.log(`dbConfig`, dbConfig); @@ -27,7 +28,15 @@ const dataSources = () => ({ }); const context = async ({ req }) => { - return { data: { foo: 'bar' } }; + const authHeader = req.headers.authorization || ''; + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7, authHeader.length); + const res = await verifyToken({ token }); + return { auth: res }; + } else if (authHeader == 'Basic ZGV2OmRldg==') { + return { auth: 'during development' }; + } + throw new AuthenticationError('Not authorized'); }; async function main() {