Skip to content

Commit

Permalink
Added tests and updated readme
Browse files Browse the repository at this point in the history
  • Loading branch information
[email protected] committed Nov 29, 2017
1 parent a752012 commit 9f9a867
Show file tree
Hide file tree
Showing 11 changed files with 3,633 additions and 82 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["env"]
}
7 changes: 7 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# These are test credentials which are acceptable to be commited to version control
AUTH0_DOMAIN=https://npm-authorizer.auth0.com/
AUTH0_CLIENT_ID=uUfz8vpypmgdhWMGd4meGA1k3feAJcPW
AUTH0_CLIENT_SECRET=gllrc0F23XgNXkbVoR5CgIc8sQyPaqxTFOHIqrQYChR68YwakjLa1FWBvaYzNjxD
AUTH0_AUDIENCE=https://test.npm-authorizer.com
AUTH0_ALGORITHM=RS256
ENVIRONMENT=test
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Installation
1. `npm install --save git+ssh://[email protected]/Vin65/npm-auth0-authorizer.git#master`

2. Ensure the following environment variables are defined within your project:
- AUTH0_DOMAIN (example: https://yourwebsite.auth0.com/)
- AUTH0_AUDIENCE (The unique identifier of the target API you want to access.)
- AUTH0_ALGORITHM (default: RS256)

# Usage

- handler.js

```javascript
import auth0Authorizer from 'npm-auth0-authorizer/methods/auth0Authorizer';

module.exports.auth0Authorizer = function(event, context, callback){
auth0Authorizer(event).then(success => {
return callback(null, success);
}).catch(err => {
return callback(err);
});
};
```

- serverless.yml

```yaml
functions:
auth0Authorizer:
handler: handler.auth0Authorizer
lambdaYouWishToProtect:
handler: handler.test
events:
- http:
path: protect-me
method: get
integration: lambda
authorizer:
name: auth0Authorizer
resultTtlInSeconds: 0
identitySource: method.request.header.Authorization
```
22 changes: 22 additions & 0 deletions lib/utilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const request = require('request-promise');

var utilities = {
isString: function(obj) {
return (typeof obj === 'string' || obj instanceof String);
},
parseJSON: function(string) {
return this.isString(string) ? JSON.parse(string) : string;
},
httpRequest: function(options) {
return request(options).then(response => {
return response;
}).catch(err => {
console.error(err.body);
return err;
});
}
}

module.exports = utilities;
33 changes: 19 additions & 14 deletions methods/auth0Authorizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,29 @@ import AuthPolicy from './../objects/AuthPolicy';
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

const auth0Authorizer = async (event) => {
const auth0Authorizer = (event) => {
let client = jwksClient({
strictSsl: true, // Default value
jwksUri : process.env.AUTH0_DOMAIN + '.well-known/jwks.json'
});

let policyResources = AuthPolicy.policyResources(event);
client.getKeys((err, key) => {
client.getSigningKey(key[0].kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
const token = event.authorizationToken.substring(7);

jwt.verify(token, signingKey, { algorithms: [process.env.AUTH0_ALGORITHM] }, (err, payload) => {
let principalId = payload ? payload.sub : 'invalidJWT';
const policy = new AuthPolicy(principalId, policyResources.awsAccountId, policyResources.apiOptions);
payload ? policy.allowAllMethods() : policy.denyAllMethods();
let authResponse = policy.build();
return authResponse;

return new Promise(function(resolve, reject){
let policyResources = AuthPolicy.policyResources(event);
client.getKeys((err, key) => {
if(err) reject(err);
client.getSigningKey(key[0].kid, (err, key) => {
if(err) reject(err);
const signingKey = key.publicKey || key.rsaPublicKey;
const token = event.authorizationToken.substring(7);

jwt.verify(token, signingKey, { algorithms: [process.env.AUTH0_ALGORITHM] }, (err, payload) => {
if(err) reject(err);
let principalId = payload ? payload.sub : 'invalidJWT';
const policy = new AuthPolicy(principalId, policyResources.awsAccountId, policyResources.apiOptions);
payload ? policy.allowAllMethods() : policy.denyAllMethods();
let authResponse = policy.build();
resolve(authResponse);
});
});
});
});
Expand Down
145 changes: 79 additions & 66 deletions objects/AuthPolicy.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,33 @@ class AuthPolicy {
this.HttpVerb = httpVerbToStringMap;
this._setApiOptions(this, apiOptions);
};

_setApiOptions(object, apiOptions = {}) {
['restApiId', 'region', 'stage'].forEach(function(property) {
object[property] = apiOptions['property'] || '*';
});
}

_validateHttpVerb(verb) {
if (verb !== '*' && !this.HttpVerb.hasOwnProperty(verb)) {
throw new Error('Invalid HTTP verb ' + verb + '. Allowed verbs in AuthPolicy.HttpVerb');

static policyResources(event) {
let awsInfo = event.methodArn.split(':');
let apiGatewayArnTmp = awsInfo[5].split('/');
let data = {
apiGatewayArnTmp: apiGatewayArnTmp,
apiOptions: {
restApiId: apiGatewayArnTmp[0],
stage: apiGatewayArnTmp[1],
region: awsInfo[3],
},
method: apiGatewayArnTmp[2],
awsAccountId: awsInfo[4],
resource: '/' //root resource
}
return true;
}

_validateResource(resource) {
if (this.pathRegex.test(resource)) return true;
throw new Error('Invalid resource path: ' + resource + '. Path should match ' + this.pathRegex);
}

_cleanResource(resource) {
return (resource.substring(0, 1) === '/') ? resource.substring(1, resource.length) : resource;
}

_resourceArnAndConditions(verb, cleanedResource, conditions) {
let resourceArn = `arn:aws:execute-api:${this.region}:${this.awsAccountId}:${this.restApiId}/${this.stage}/${verb}/${cleanedResource}`;
return { resourceArn: resourceArn, conditions: conditions };

if (data.apiGatewayArnTmp[3]) data.resource += data.apiGatewayArnTmp.slice(3, data.apiGatewayArnTmp.length).join('/');

return data;
};

build() {
if (this._hasNoMethods()) throw new Error('No statements defined for the policy');
let policy = { principalId: this.principalId };
let doc = { Version: this.version, Statement: this._allowAndDenyStatements() };
policy.policyDocument = doc;
return policy;
}

addMethod(effect, verb, resource, conditions) {
Expand All @@ -70,38 +71,7 @@ class AuthPolicy {

return statement;
};

_methodDoesNotHaveConditions(method) {
return (method.conditions === null || method.conditions.length === 0);
}

_conditionalStatement(effect, method) {
let conditionalStatement = this.getEmptyStatement(effect);
conditionalStatement.Resource.push(method.resourceArn);
conditionalStatement.Condition = method.conditions;
return conditionalStatement;
}

_nonConditionalStatement(effect, method) {
let conditionalStatement = this.getEmptyStatement(effect);
conditionalStatement.Resource.push(method.resourceArn);
return conditionalStatement;
}

_statementsFromMethods(effect, methods) {
return methods.reduce((statements, method) => {
let statement =
(this._methodDoesNotHaveConditions(method)) ? this._nonConditionalStatement(effect, method) : this._conditionalStatement(effect, method);

statements.push(statement);
return statements;
}, []);
}

_hasResources(statement) {
return (statement.Resource !== null && statement.Resource.length > 0);
}


getStatementsForEffect(effect, methods) {
if (methods.length === 0) return [];

Expand Down Expand Up @@ -135,6 +105,57 @@ class AuthPolicy {
denyMethodWithConditions(verb, resource, conditions) {
this.addMethod.call(this, 'deny', verb, resource, conditions);
};

_setApiOptions(object, apiOptions = {}) {
['restApiId', 'region', 'stage'].forEach(function(property) {
object[property] = apiOptions['property'] || '*';
});
}

_validateHttpVerb(verb) {
if (verb !== '*' && !this.HttpVerb.hasOwnProperty(verb)) {
throw new Error('Invalid HTTP verb ' + verb + '. Allowed verbs in AuthPolicy.HttpVerb');
}
return true;
}

_validateResource(resource) {
if (this.pathRegex.test(resource)) return true;
throw new Error('Invalid resource path: ' + resource + '. Path should match ' + this.pathRegex);
}

_cleanResource(resource) {
return (resource.substring(0, 1) === '/') ? resource.substring(1, resource.length) : resource;
}

_resourceArnAndConditions(verb, cleanedResource, conditions) {
let resourceArn = `arn:aws:execute-api:${this.region}:${this.awsAccountId}:${this.restApiId}/${this.stage}/${verb}/${cleanedResource}`;
return { resourceArn: resourceArn, conditions: conditions };
}

_methodHasConditions(method) {
return (method.conditions !== null && method.conditions.length > 0);
}

_generateStatement(effect, method, addConditional) {
let conditionalStatement = this.getEmptyStatement(effect);
conditionalStatement.Resource.push(method.resourceArn);
if(addConditional) conditionalStatement.Condition = method.conditions;
return conditionalStatement;
}

_statementsFromMethods(effect, methods) {
return methods.reduce((statements, method) => {
let statement = this._generateStatement(effect, method, this._methodHasConditions(method));

statements.push(statement);
return statements;
}, []);
}

_hasResources(statement) {
return (statement.Resource !== null && statement.Resource.length > 0);
}

_hasNoMethods() {
let hasNoAllowMethods = (!this.allowMethods || this.allowMethods.length === 0);
Expand All @@ -144,17 +165,9 @@ class AuthPolicy {

_allowAndDenyStatements() {
let allow = this.getStatementsForEffect.call(this, 'Allow', this.allowMethods);
let deny = this.getStatementsForEffect.call(this, "Deny", this.denyMethods);
let deny = this.getStatementsForEffect.call(this, 'Deny', this.denyMethods);
return allow.concat(deny);
}

build() {
if (this._hasNoMethods()) throw new Error('No statements defined for the policy');
let policy = { principalId: this.principalId };
let doc = { Version: this.version, Statement: this._allowAndDenyStatements() };
policy.policyDocument = doc;
return policy;
}
};

module.exports = AuthPolicy;
Loading

0 comments on commit 9f9a867

Please sign in to comment.