aws-apigw-authorizer
Advanced tools
Comparing version 0.1.13 to 0.1.14
import * as AWSLambda from './lambda'; | ||
export declare type PolicyBuilderFunction = (event: AWSLambda.CustomAuthorizerEvent, principal: string, decodedToken?: Jwt) => AWSLambda.PolicyDocument; | ||
export declare type ContextBuilderFunction = (event: AWSLambda.CustomAuthorizerEvent, principal: string, decodedToken?: Jwt) => AWSLambda.AuthResponseContext; | ||
export declare type PolicyBuilderFunction = (event: AWSLambda.CustomAuthorizerEvent, principalId: string, decodedToken?: Jwt) => AWSLambda.PolicyDocument | Promise<AWSLambda.PolicyDocument>; | ||
export declare type ContextBuilderFunction = (event: AWSLambda.CustomAuthorizerEvent, principalId: string, decodedToken?: Jwt) => AWSLambda.AuthResponseContext | Promise<AWSLambda.AuthResponseContext> | void; | ||
export declare type CustomAuthChecksFunction = (event: AWSLambda.CustomAuthorizerEvent, principalId: string, decodedToken?: Jwt) => void | Promise<void>; | ||
export declare type PrincipalId = string; | ||
export declare type JwtPrincipalIdSelectorFunction = (event: AWSLambda.CustomAuthorizerEvent, decodedToken?: Jwt) => PrincipalId | Promise<PrincipalId>; | ||
export interface AuthorizerConfig { | ||
policyBuilder?: PolicyBuilderFunction; | ||
contextBuilder?: ContextBuilderFunction; | ||
customAuthChecks?: CustomAuthChecksFunction; | ||
jwtPrincipalIdSelectorFunction?: JwtPrincipalIdSelectorFunction; | ||
} | ||
@@ -12,11 +17,13 @@ export declare type Jwt = string | object; | ||
private contextBuilder; | ||
private customAuthChecks; | ||
private basicAuthenticationEnabled; | ||
private jwtAuthenticationEnabled; | ||
private principalIdSelectorFunction; | ||
constructor(authorizerConfig?: AuthorizerConfig); | ||
private assertSourceIp(event); | ||
private authorize(event, principal, decodedToken?, ...logMessages); | ||
private deny(event, ...logMessages); | ||
private log(event, ...logMessages); | ||
private determineAuthorization(event); | ||
private assertSourceIp; | ||
private authorize; | ||
private deny; | ||
private log; | ||
private determineAuthorization; | ||
handler(event: AWSLambda.CustomAuthorizerEvent, _context: AWSLambda.Context, callback: AWSLambda.Callback): Promise<void>; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const basicAuthValidator = require("./basic-auth-validator"); | ||
const ipRangeCheck = require("ip-range-check"); | ||
const envkey_1 = require("./envkey"); | ||
const jwtValidator = require("./jwt-validator"); | ||
const preventBruteForce = require("./prevent-brute-force"); | ||
const awsPolicyLib = require('./aws-policy-lib'); | ||
@@ -18,4 +15,8 @@ // It is mandatory to set up ALLOWED_IP_ADDRESSES (0.0.0.0/0 is allowed) | ||
this.jwtAuthenticationEnabled = false; | ||
// parse config | ||
this.policyBuilder = authorizerConfig && authorizerConfig.policyBuilder || defaultBuildPolicy; | ||
this.contextBuilder = authorizerConfig && authorizerConfig.contextBuilder || defaultBuildContext; | ||
this.contextBuilder = authorizerConfig && authorizerConfig.contextBuilder || (() => undefined); | ||
this.customAuthChecks = authorizerConfig && authorizerConfig.customAuthChecks || (() => undefined); | ||
this.principalIdSelectorFunction = authorizerConfig && authorizerConfig.jwtPrincipalIdSelectorFunction || defaultJwtPrincipalIdSelector; | ||
// check environment for configured auth flavors | ||
if (Object.keys(process.env).filter(key => key.startsWith('BASIC_AUTH_USER_')).length) { | ||
@@ -35,8 +36,11 @@ this.basicAuthenticationEnabled = true; | ||
} | ||
authorize(event, principal, decodedToken, ...logMessages) { | ||
const context = this.contextBuilder(event, principal, decodedToken); | ||
const policy = Object.assign({}, this.policyBuilder(event, principal, decodedToken), { context }); | ||
async authorize(event, principalId, decodedToken, ...logMessages) { | ||
await this.customAuthChecks(event, principalId, decodedToken); | ||
const policy = await this.policyBuilder(event, principalId, decodedToken); | ||
const context = await this.contextBuilder(event, principalId, decodedToken); | ||
if (context) { | ||
Object.assign(policy, { context }); | ||
} | ||
this.log(event, 'Authorized:', ...logMessages); | ||
this.log(event, 'Built policy:', JSON.stringify(policy)); | ||
preventBruteForce.registerAuthorization(this.assertSourceIp(event)); | ||
return policy; | ||
@@ -46,3 +50,2 @@ } | ||
this.log(event, 'Denied:', ...logMessages); | ||
preventBruteForce.registerDenial(this.assertSourceIp(event)); | ||
return 'Unauthorized'; | ||
@@ -54,51 +57,44 @@ } | ||
} | ||
determineAuthorization(event) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
console.log(JSON.stringify(event, undefined, 2)); | ||
// 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); | ||
// Sanity check: the callers sourceIp should not be attempting a brute force | ||
preventBruteForce.checkBruteForceAttempt(sourceIp); | ||
// Sanity check: the callers sourceIp should be an allowed ip | ||
if (envkey_1.envkey('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 = yield jwtValidator.validate(token); | ||
const principal = decodedToken['upn'] || decodedToken['email']; | ||
return this.authorize(event, principal, decodedToken, `user ${principal} using JWT`); | ||
} | ||
else if (tokenType === 'Basic' && this.basicAuthenticationEnabled) { | ||
const creds = basicAuthValidator.validate(token); | ||
return this.authorize(event, creds.name, undefined, `user ${creds.name} using Basic Auth`); | ||
} | ||
else { | ||
throw new Error(`Unauthorized: unsupported token type ${tokenType}`); | ||
} | ||
}); | ||
async determineAuthorization(event) { | ||
// 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); | ||
// 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}`); | ||
} | ||
} | ||
handler(event, _context, callback) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const policy = yield this.determineAuthorization(event); | ||
callback(undefined, policy); | ||
} | ||
catch (err) { | ||
callback(this.deny(event, err)); | ||
} | ||
}); | ||
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, principal, _decodedToken) { | ||
// this function must generate a policy that is associated with the recognized principal user identifier. | ||
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 | ||
@@ -131,9 +127,17 @@ // keep in mind, the policy is cached for 5 minutes by default (TTL is configurable in the authorizer) | ||
// otherwise it would be denied | ||
const policy = new awsPolicyLib.AuthPolicy(principal, awsAccountId, apiOptions); | ||
const policy = new awsPolicyLib.AuthPolicy(principalId, awsAccountId, apiOptions); | ||
policy.allowMethodWithConditions(awsPolicyLib.AuthPolicy.HttpVerb.ALL, '/*', conditions); | ||
return policy.build(); | ||
} | ||
function defaultBuildContext(_event, principal, _decodedToken) { | ||
return { principal }; | ||
function defaultJwtPrincipalIdSelector(_event, decodedToken) { | ||
let principalId = 'Undeterminable Principal'; | ||
if (decodedToken) { | ||
// Different identity providers put different claims on tokens | ||
// Auth0 seems to always put 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'] || principalId; | ||
} | ||
return principalId; | ||
} | ||
//# sourceMappingURL=authorizer.js.map |
@@ -0,0 +0,0 @@ /** |
export declare function validate(token: string): any; |
@@ -0,0 +0,0 @@ "use strict"; |
declare module 'ip-range-check'; |
export declare function envkey(key: string): string; |
@@ -0,0 +0,0 @@ "use strict"; |
export declare function validate(jwtToken: string): Promise<string | object>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const jwt = require("jsonwebtoken"); | ||
const jwksClient = require("jwks-rsa"); | ||
const envkey_1 = require("./envkey"); | ||
let _jwksClient; | ||
let _jwksClientUri; | ||
function getSigningKey(jwksUri, kid) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
if (!_jwksClient || jwksUri !== _jwksClientUri || process.env.JWKS_NO_CACHE) { | ||
_jwksClientUri = jwksUri; | ||
_jwksClient = jwksClient({ cache: true, rateLimit: true, jwksUri }); | ||
} | ||
return new Promise((resolve, reject) => { | ||
_jwksClient.getSigningKey(kid, (err, jwk) => err ? reject(err) : resolve(jwk)); | ||
}); | ||
async function getSigningKey(jwksUri, kid) { | ||
if (!_jwksClient || jwksUri !== _jwksClientUri || process.env.JWKS_NO_CACHE) { | ||
_jwksClientUri = jwksUri; | ||
_jwksClient = jwksClient({ cache: true, rateLimit: true, jwksUri }); | ||
} | ||
return new Promise((resolve, reject) => { | ||
_jwksClient.getSigningKey(kid, (err, jwk) => err ? reject(err) : resolve(jwk)); | ||
}); | ||
} | ||
function validate(jwtToken) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const expectedAudience = envkey_1.envkey('AUDIENCE_URI'); | ||
const expectedIssuer = envkey_1.envkey('ISSUER_URI'); | ||
const jwksUri = envkey_1.envkey('JWKS_URI'); | ||
const decodedJwtToken = jwt.decode(jwtToken, { complete: true }); | ||
if (!decodedJwtToken) { | ||
throw new Error('Cannot parse JWT token'); | ||
} | ||
const kid = decodedJwtToken['header'] && decodedJwtToken['header']['kid']; | ||
const jwk = yield getSigningKey(jwksUri, kid); | ||
const signingKey = jwk.publicKey || jwk.rsaPublicKey; | ||
if (!signingKey) { | ||
throw new Error('Cannot determine the key with which the token was signed'); | ||
} | ||
const verificationOptions = { | ||
audience: expectedAudience, | ||
issuer: expectedIssuer, | ||
ignoreExpiration: false | ||
}; | ||
// For testing purposes JWT expiration can be disregarded using an environment variable | ||
if (['1', 'true', 'TRUE', 'True'].indexOf(process.env.JWT_NO_EXPIRATION || '') > -1) { | ||
verificationOptions.ignoreExpiration = true; | ||
} | ||
// Verify the JWT | ||
// This either rejects (JWT not valid), or resolves withe the decoded token (object or string) | ||
return new Promise((resolve, reject) => { | ||
jwt.verify(jwtToken, signingKey, verificationOptions, (err, decodedJwtToken) => err ? reject(err) : resolve(decodedJwtToken)); | ||
}); | ||
async function validate(jwtToken) { | ||
const expectedAudience = process.env.AUDIENCE_URI || ''; | ||
const expectedIssuer = process.env.ISSUER_URI || ''; | ||
const jwksUri = process.env.JWKS_URI || ''; | ||
const decodedJwtToken = jwt.decode(jwtToken, { complete: true }); | ||
if (!decodedJwtToken) { | ||
throw new Error('Cannot parse JWT token'); | ||
} | ||
const kid = decodedJwtToken['header'] && decodedJwtToken['header']['kid']; | ||
const jwk = await getSigningKey(jwksUri, kid); | ||
const signingKey = jwk.publicKey || jwk.rsaPublicKey; | ||
if (!signingKey) { | ||
throw new Error('Cannot determine the key with which the token was signed'); | ||
} | ||
const verificationOptions = { | ||
audience: expectedAudience, | ||
issuer: expectedIssuer, | ||
ignoreExpiration: false | ||
}; | ||
// For testing purposes JWT expiration can be disregarded using an environment variable | ||
if (['1', 'true', 'TRUE', 'True'].indexOf(process.env.JWT_NO_EXPIRATION || '') > -1) { | ||
verificationOptions.ignoreExpiration = true; | ||
} | ||
// Verify the JWT | ||
// This either rejects (JWT not valid), or resolves withe the decoded token (object or string) | ||
return new Promise((resolve, reject) => { | ||
jwt.verify(jwtToken, signingKey, verificationOptions, (err, decodedJwtToken) => err ? reject(err) : resolve(decodedJwtToken)); | ||
}); | ||
@@ -50,0 +44,0 @@ } |
@@ -0,0 +0,0 @@ // This all stolen and very slightly adapted from: @types/aws-lambda |
{ | ||
"name": "aws-apigw-authorizer", | ||
"version": "0.1.13", | ||
"version": "0.1.14", | ||
"description": "AWS Lambda Authorizer for API Gateway", | ||
@@ -12,7 +12,7 @@ "main": "lib/authorizer.js", | ||
"tsc": "tsc", | ||
"build": "rm -rf lib && tsc && cp src/*.d.ts src/*.js lib" | ||
"build": "npm run cover && rm -rf lib && tsc && cp src/*.d.ts src/*.js lib" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://bitbucket.org/mnservices/aws-apigw-authorizer.git" | ||
"url": "git+https://github.com/ottokruse/aws-apigw-authorizer.git" | ||
}, | ||
@@ -25,31 +25,31 @@ "author": "pi-team@mn.nl", | ||
"license": "ISC", | ||
"homepage": "https://bitbucket.org/mnservices/aws-apigw-authorizer#readme", | ||
"homepage": "https://github.com/ottokruse/aws-apigw-authorizer", | ||
"dependencies": { | ||
"basic-auth": "^2.0.0", | ||
"ip-range-check": "0.0.2", | ||
"jsonwebtoken": "^8.1.0", | ||
"jsonwebtoken": "^8.3.0", | ||
"jwks-rsa": "^1.2.1", | ||
"tslib": "^1.8.1" | ||
"tslib": "^1.9.2" | ||
}, | ||
"devDependencies": { | ||
"@types/chai": "^4.1.0", | ||
"@types/chai": "^4.1.4", | ||
"@types/chai-as-promised": "^7.1.0", | ||
"@types/jsonwebtoken": "^7.2.5", | ||
"@types/mocha": "^2.2.46", | ||
"@types/nock": "^9.1.1", | ||
"@types/node": "^9.3.0", | ||
"@types/jsonwebtoken": "^7.2.7", | ||
"@types/mocha": "^5.2.2", | ||
"@types/nock": "^9.1.3", | ||
"@types/node": "^10.3.3", | ||
"chai": "^4.1.2", | ||
"chai-as-promised": "^7.1.1", | ||
"mocha": "^3.5.3", | ||
"nock": "^9.1.6", | ||
"nyc": "^11.4.1", | ||
"ts-node": "^4.1.0", | ||
"typescript": "^2.6.2" | ||
"mocha": "^5.2.0", | ||
"nock": "^9.3.3", | ||
"nyc": "^12.0.2", | ||
"ts-node": "^6.1.1", | ||
"typescript": "^2.9.2" | ||
}, | ||
"nyc": { | ||
"check-coverage": true, | ||
"lines": 90, | ||
"statements": 90, | ||
"functions": 90, | ||
"branches": 90, | ||
"lines": 85, | ||
"statements": 85, | ||
"functions": 85, | ||
"branches": 85, | ||
"include": [ | ||
@@ -56,0 +56,0 @@ "src/*.ts", |
# AWS Lambda Authorizer for API Gateway | ||
## This is a barebone AWS Lambda Authorizer for API Gateway. | ||
## This is an AWS Lambda Authorizer for API Gateway | ||
It can be used as-is, in which case a default AWS IAM policy is used that allows access to all resources in the API using any HTTP method. | ||
This is an implementation in NodeJS of a custom authorizer function for AWS API Gateway. (https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) | ||
Configure through Lambda environment variables (see below). | ||
This custom authorizer supports these authentication mechanisms: | ||
## Implement an API Gateway Authorizer Lambda functions as follows: | ||
- JWT | ||
- Basic Authentication | ||
In the default configuration this authorizer will grant the user access to invoke all resources of the API using any HTTP method. | ||
Configuration can be provided through Lambda environment variables (see below). | ||
## How to use | ||
Create a Lambda function in AWS using *Node 8.10* runtime and use the following code: | ||
```js | ||
@@ -17,5 +26,14 @@ const lambdaAuthorizer = new (require('aws-apigw-authorizer')).ApiGatewayAuthorizer(); | ||
Of course you will have to create a deployment package to include `aws-apigw-authorizer` and it's dependencies. | ||
npm install aws-apigw-authorizer | ||
See instructions here: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-create-deployment-pkg.html | ||
### Custom Policy Builder | ||
A custom function can be provided for building custom AWS IAM policies. The custom function will be called after succesfull authentication: | ||
```js | ||
// May return promise or synchronous result as below | ||
function customPolicyBuilder(event, principal, decodedJwt) { | ||
@@ -39,3 +57,3 @@ // event: the raw event that the authorizer lambda function receives from API Gateway | ||
"aws:SourceIp": [ | ||
"213.149.225.141/32" | ||
"123.456.789.123/32" | ||
] | ||
@@ -50,3 +68,5 @@ } | ||
const lambdaAuthorizer = new (require('aws-apigw-authorizer')).ApiGatewayAuthorizer({ policyBuilder: customPolicyBuilder }); | ||
const lambdaAuthorizer = new (require('aws-apigw-authorizer')).ApiGatewayAuthorizer( | ||
{ policyBuilder: customPolicyBuilder } | ||
); | ||
@@ -56,8 +76,11 @@ exports.handler = lambdaAuthorizer.handler.bind(lambdaAuthorizer); | ||
A custom function can be provided for setting the authorization context. The custom function will be called after succesfull authentication: | ||
### Custom Context Builder | ||
A custom function can be provided for setting the authorization context (https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html). The custom function will be called after succesfull authentication: | ||
```js | ||
function customContextBuilder(_event, _principal, decodedToken) { | ||
// May return promise or synchronous result as below | ||
function customContextBuilder(event, principal, decodedToken) { | ||
return { | ||
sub: decodedToken['sub'], | ||
name: decodedToken['sub'], | ||
foo: 'bar' | ||
@@ -67,3 +90,5 @@ } | ||
const authorizer = new(require('aws-apigw-authorizer')).ApiGatewayAuthorizer({ contextBuilder: customContextBuilder}); | ||
const authorizer = new (require('aws-apigw-authorizer')).ApiGatewayAuthorizer( | ||
{ contextBuilder: customContextBuilder } | ||
); | ||
@@ -73,3 +98,23 @@ exports.handler = authorizer.handler.bind(authorizer); | ||
If you throw an error anywhere in the customContextBuilder the request will be denied (HTTP 401). | ||
### Custom Auth Checks | ||
A custom function can be provided in which you can include your own checks. If you throw an error anywhere in that function the request will be denied (HTTP 401). | ||
```js | ||
// May return promise or synchronous result as below | ||
function customAuthChecks(event, principal, decodedToken) { | ||
if (!event.headers['X-SHOULD-BE-PRESENT']) { | ||
throw new Error('HTTP header X-SHOULD-BE-PRESENT is required'); | ||
} | ||
} | ||
const authorizer = new (require('aws-apigw-authorizer')).ApiGatewayAuthorizer( | ||
{ authChecks: customAuthChecks } | ||
); | ||
exports.handler = authorizer.handler.bind(authorizer); | ||
``` | ||
## Configuration through environment variables: | ||
@@ -76,0 +121,0 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
0
1
153
34746
13
631
23
Updatedjsonwebtoken@^8.3.0
Updatedtslib@^1.9.2