UNPKG

microservice-utilities

Version:

Utilities supporting authorization, request logging and other often reused parts of microservices.

173 lines (154 loc) 6.28 kB
const axios = require('axios'); const jwtManager = require('jsonwebtoken'); const jwkConverter = require('jwk-to-pem'); class Authorizer { /** * Constructor * @param {Function} logFunction log function, defaults to console.log * @param {Object} configuration configuration object * @param {String} configuration.jwkKeyListUrl url used to retrieve the jwk public keys * @param {String} configuration.authorizerContextResolver function to populate the authorizer context, by default it will only contain the JWT. function(identity, token) {} * @param {String} configuration.usagePlan An AWS Api Gateway usage plan ID, this will create an api key and attach it to the returned policy document. * @param {String} configuration.jwtVerifyOptions parameters that are passed to the JWT verification options. Defaults to { algorithms: ['RS256'] } */ constructor(logFunction, configuration = {}) { this.logFunction = logFunction || console.log; if (!configuration.jwkKeyListUrl) { throw new Error('Authorizer configuration error: missing required property "jwkKeyListUrl"'); } this.configuration = configuration; this.publicKeysPromise = null; this.jwtVerifyOptions = configuration.jwtVerifyOptions || { algorithms: ['RS256'] }; } async getPublicKeyPromise(kid) { if (!this.publicKeysPromise) { this.publicKeysPromise = axios.get(this.configuration.jwkKeyListUrl); } let result = null; try { result = await this.publicKeysPromise; } catch (error) { this.publicKeysPromise = null; this.logFunction({ level: 'ERROR', title: 'InternalServerError', details: 'Failed to get public key', error: error }); throw new Error('InternalServerError'); } let jwk = result.data.keys && result.data.keys.find(key => key.kid === kid); if (jwk) { return jwkConverter(jwk); } this.publicKeysPromise = null; this.logFunction({ level: 'WARN', title: 'Unauthorized', details: 'KID not found in public key list.', kid: kid || 'NO_KID_SPECIFIED', keys: result.data.keys }); throw new Error('Unauthorized'); } async ensureApiKey(clientId) { const aws = require('aws-sdk'); const apiGateway = new aws.APIGateway(); let apiKey; try { const apiKeys = await apiGateway.getApiKeys({ nameQuery: clientId, includeValues: true, limit: 1 }).promise(); apiKey = apiKeys.items[0]; } catch (e) { this.logFunction({ level: 'ERROR', title: 'FailedToApiKeys', details: 'An error occurred while fetching api keys', clientId: clientId, error: e }); } if (apiKey && apiKey.id) { return apiKey; } this.logFunction({ level: 'INFO', title: 'ApiKeyNotFound', details: 'No api key has been found, attempting to create one.', clientId: clientId }); const newKey = await apiGateway.createApiKey({ description: `Key for client ${clientId}`, enabled: true, generateDistinctId: true, name: clientId, value: clientId }).promise(); return apiGateway.createUsagePlanKey({ keyId: newKey.id, usagePlanId: this.configuration.usagePlan, keyType: 'API_KEY' }).promise(); } getCliendId(identity) { const principalId = identity.sub; return principalId.endsWith('@clients') ? principalId.split('@')[0] : identity.azp; } async getPolicy(request) { let methodArn = request.methodArn; let token = this.getTokenFromAuthorizationHeader(request); if (!token) { this.logFunction({ level: 'WARN', title: 'Unauthorized', details: 'No token specified', method: methodArn, data: request }); throw new Error('Unauthorized'); } let unverifiedToken = jwtManager.decode(token, { complete: true }); if (!unverifiedToken) { this.logFunction({ level: 'WARN', title: 'Unauthorized', details: 'Invalid token', method: methodArn, token, data: request }); throw new Error('Unauthorized'); } let kid = unverifiedToken && unverifiedToken.header && unverifiedToken.header.kid; if (!kid) { this.logFunction({ level: 'WARN', title: 'Unauthorized', details: 'Token did no provide a KID', method: methodArn, token, data: request }); throw new Error('Unauthorized'); } let key = await this.getPublicKeyPromise(kid); let identity; try { identity = await jwtManager.verify(token, key, this.jwtVerifyOptions); } catch (exception) { this.logFunction({ level: 'WARN', title: 'Unauthorized', details: 'Error verifying token', error: exception, method: methodArn, token, data: request }); throw new Error('Unauthorized'); } this.logFunction({ level: 'INFO', title: 'Verified Token', data: request }); let resolver = this.configuration.authorizerContextResolver || (() => ({ jwt: token })); const policy = { principalId: identity.sub, policyDocument: { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: [ 'execute-api:Invoke' ], Resource: [ 'arn:aws:execute-api:*:*:*' ] } ] }, context: resolver(identity, token) }; if (this.configuration.usagePlan) { policy.usageIdentifierKey = this.getCliendId(identity); try { await this.ensureApiKey(policy.usageIdentifierKey); } catch (e) { this.logFunction({ level: 'Error', title: 'FailedToEnsureApiKey', details: 'Failed to ensure that an api key exists', clientId: policy.usageIdentifierKey, error: e }); } } return policy; } getTokenFromAuthorizationHeader(request) { let authorizationHeaderKey = Object.keys(request.headers || {}) .find(headerKey => headerKey.toLowerCase() === 'authorization') || null; let authorizationHeader = authorizationHeaderKey ? request.headers[authorizationHeaderKey] : null; let authorizationHeaderFragments = authorizationHeader ? authorizationHeader.split(' ') : []; return authorizationHeaderFragments.length === 2 ? authorizationHeaderFragments[1] : null; } } module.exports = Authorizer;