UNPKG

aws-apigw-authorizer

Version:
140 lines (139 loc) 7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const basicAuthValidator = require("./basic-auth-validator"); const ipRangeCheck = require('ip-range-check'); // no typings available const jwtValidator = require("./jwt-validator"); const awsPolicyLib = require('./aws-policy-lib'); // plain JS file class ApiGatewayAuthorizer { constructor(authorizerConfig) { this.basicAuthenticationEnabled = false; this.jwtAuthenticationEnabled = false; // parse config this.policyBuilder = authorizerConfig && authorizerConfig.policyBuilder || defaultBuildPolicy; this.contextBuilder = authorizerConfig && authorizerConfig.contextBuilder || (() => undefined); this.authChecks = authorizerConfig && authorizerConfig.authChecks || (() => undefined); this.principalIdSelectorFunction = authorizerConfig && authorizerConfig.jwtPrincipalIdSelector || defaultJwtPrincipalIdSelector; // check environment for configured auth flavors if (Object.keys(process.env).filter(key => key.startsWith('BASIC_AUTH_USER_')).length) { this.basicAuthenticationEnabled = true; } if (process.env.AUDIENCE_URI && process.env.ISSUER_URI && process.env.JWKS_URI) { this.jwtAuthenticationEnabled = true; } } assertSourceIp(event) { const sourceIp = event.requestContext && event.requestContext.identity.sourceIp; if (!sourceIp) { throw new Error('Source IP Cannot be determined'); } return sourceIp; } async authorize(event, principalId, decodedToken, ...logMessages) { await this.authChecks(event, principalId, decodedToken); const context = await this.contextBuilder(event, principalId, decodedToken); const policy = await this.policyBuilder(event, principalId, decodedToken); if (context) { Object.assign(policy, { context }); } this.log(event, 'Authorized:', ...logMessages); this.log(event, 'Built policy:', JSON.stringify(policy)); return policy; } deny(event, ...logMessages) { this.log(event, 'Denied:', ...logMessages); return 'Unauthorized'; } log(event, ...logMessages) { const sourceIp = this.assertSourceIp(event); console.log(`${[sourceIp, ...logMessages].join(' ')}`); } async determineAuthorization(event) { // Change headers to lowercase to support HTTP/2. See https://tools.ietf.org/html/rfc7540#section-8.1.2 event.headers = toLowerCaseKeys(event.headers); // Sanity check: the Authorization header must be present if (!event.headers || !event.headers.authorization) { throw new Error('Authorization HTTP header not present'); } // Sanity check: the callers sourceIp should be present const sourceIp = this.assertSourceIp(event); // It is mandatory to set up ALLOWED_IP_ADDRESSES (0.0.0.0/0 is allowed) if (!process.env.ALLOWED_IP_ADDRESSES) { throw new Error('Cannot accept any source IP as ALLOWED_IP_ADDRESSES has not been set'); } // Sanity check: the callers sourceIp should be an allowed ip if (process.env.ALLOWED_IP_ADDRESSES .split(',') .filter((ipRange) => ipRangeCheck(sourceIp, ipRange)) .length === 0) { throw new Error('Source IP does not match with configured ALLOWED_IP_ADDRESSES'); } // Validate credentials const [tokenType, token] = event.headers.authorization.split(' '); if (tokenType === 'Bearer' && this.jwtAuthenticationEnabled) { const decodedToken = await jwtValidator.validate(token); const principalId = await this.principalIdSelectorFunction(event, decodedToken); return await this.authorize(event, principalId, decodedToken, `user ${principalId} using JWT`); } else if (tokenType === 'Basic' && this.basicAuthenticationEnabled) { const principalId = basicAuthValidator.validate(token).name; return await this.authorize(event, principalId, undefined, `user ${principalId} using Basic Auth`); } else { throw new Error(`Unauthorized: unsupported token type ${tokenType}`); } } async handler(event, _context, callback) { try { const policy = await this.determineAuthorization(event); callback(undefined, policy); } catch (err) { callback(this.deny(event, err)); } } } exports.ApiGatewayAuthorizer = ApiGatewayAuthorizer; function defaultBuildPolicy(event, principalId, _decodedToken) { // this function must generate a policy that is associated with the recognized principalId user identifier. // depending on your use case, you might store policies in a DB, or generate them on the fly // keep in mind, the policy is cached for 5 minutes by default (TTL is configurable in the authorizer) // and will apply to subsequent calls to any method/resource in the RestApi // made with the same token // you can send a 401 Unauthorized response to the client by failing like so: // callback('Unauthorized'); // if access is denied, the client will recieve a 403 Access Denied response // if access is allowed, API Gateway will proceed with the backend integration configured on the method that was called // build apiOptions for the AuthPolicy const tmp = event.methodArn.split(':'); const apiGatewayArnTmp = tmp[5].split('/'); const awsAccountId = tmp[4]; const apiOptions = { region: tmp[3], restApiId: apiGatewayArnTmp[0], stage: apiGatewayArnTmp[1], }; // Allow access to all methods on the entire API // Such a wildcard is necessary in case of authorization caching because on the second call // a different resource or method may be used, which needs to be covered by the cached policy // otherwise it would be denied const policy = new awsPolicyLib.AuthPolicy(principalId, awsAccountId, apiOptions); policy.allowMethod(awsPolicyLib.AuthPolicy.HttpVerb.ALL, '/*'); return policy.build(); } function defaultJwtPrincipalIdSelector(_event, decodedToken) { let principalId; if (decodedToken) { // Different identity providers put different claims on tokens // Auth0 seems to always include the 'email' claim // Microsoft seems to always put the e-mail address in 'upn' claim // Last resort is the 'sub' claim which should mostly be present but contains an ID specific to the identity provider principalId = decodedToken['email'] || decodedToken['upn'] || decodedToken['sub']; } return principalId || 'Undeterminable Principal'; } function toLowerCaseKeys(obj) { return Object.keys(obj).reduce((accum, key) => { accum[key.toLowerCase()] = obj[key]; return accum; }, {}); }