aws-apigw-authorizer
Version:
AWS Lambda Authorizer for API Gateway
140 lines (139 loc) • 7 kB
JavaScript
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;
}, {});
}
;