Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
dist
.npmrc
*.swp
.DS_Store
120 changes: 70 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,68 @@

# OAuth 2.0 Bearer JWT Authorizer for AWS API Gateway

This project is sample implementation of an AWS Lambda custom authorizer for [AWS API Gateway](https://aws.amazon.com/api-gateway/) that works with a JWT bearer token (`id_token` or `access_token`) issued by an OAuth 2.0 Authorization Server. It can be used to secure access to APIs managed by [AWS API Gateway](https://aws.amazon.com/api-gateway/).
This project is a sample Node.js implementation of an AWS Lambda custom authorizer for [AWS API Gateway](https://aws.amazon.com/api-gateway/) that works with a JWT bearer token (`id_token` or `access_token`) issued by an OAuth 2.0 Authorization Server. It can be used to secure access to APIs managed by [AWS API Gateway](https://aws.amazon.com/api-gateway/).

## Configuration
It has been designed to work with Okta but should work with any OAuth 2.0 Authorization Server.

### Environment Variables (.env)
The authorizer uses Okta's [JWT Verifier library](https://github.com/okta/okta-oidc-js/tree/master/packages/jwt-verifier) to retrieve keys (jwks) from your Okta tenant and verify tokens.

Update the `ISSUER` and `AUDIENCE` variables in the `.env` file
## Prerequisites

```
ISSUER=https://example.oktapreview.com/oauth2/aus8o56xh1qncrlwT0h7
AUDIENCE=https://api.example.com
```
There are three main components to this setup:

It is critical that the `issuer` and `audience` claims for JWT bearer tokens are [properly validated using best practices](http://www.cloudidentity.com/blog/2014/03/03/principles-of-token-validation/). You can obtain these values from your OAuth 2.0 Authorization Server configuration.
1. An Okta tenant, with an Authorization Server
If you do not already have an Okta tenant with an Authorization Server, you can get a free-forever developer tenant from Okta that has all the capabilities you need for this repo [here](https://developer.okta.com).

The `audience` value should uniquely identify your AWS API Gateway deployment. You should assign unique audiences for each API Gateway authorizer instance so that a token intended for one gateway is not valid for another.
2. Amazon API Gateway

### Signature Keys (keys.json)
3. An AWS Lambda function (this repo) to perform token validation

Update `keys.json` with the JSON Web Key Set (JWKS) format for your issuer. You can usually obtain the JWKS for your issuer by fetching the `jwks_uri` published in your issuer's metadata such as `${issuer}/.well-known/openid-configuration`.
(You will also need an IAM role for the Lambda function.)

> The authorizer only supports RSA signature keys
## Lambda authorizer quick setup - overview

> Ensure that your issuer uses a pinned key for token signatures and does not automatically rotate signing keys. The authorizer currently does not support persistence of cached keys (e.g. dynamo) obtained via metadata discovery.
The easiest way to get up and running with this authorizer is to create a new Lambda function using the publicly available S3 bucket as your code source, and add your environment variables to the function via the Lambda UI.

### Scopes
This default authorizer enforces scope-based access to API resources using the `scp` claim in the JWT. The `api:read` scope is required for `GET` requests and `api:write` scope for `POST`, `PUT`, `PATCH`, or `DELETE` requests.

This sample currently enforces scope-based access to API resources using the `scp` claim in the JWT. The `api:read` scope is required for `GET` requests and `api:write` scope for `POST`, `PUT`, `PATCH`, or `DELETE` requests.
To customize the authorizer, please see "Customizing the authorizer" below.

Update `index.js` with your authorization requirements and return the resulting AWS IAM Policy for the request.
## Lambda authorizer quick setup - steps

# Deployment
From the [AWS Lambda console](https://console.aws.amazon.com/lambda/home#/create?step=2)

### Install Dependencies
* Author from scratch

Run `npm install` to download all of the authorizer's dependent modules. This is a prerequisite for deployment as AWS Lambda requires these files to be included in the uploaded bundle.
* **Function name:** oauth2-jwt-authorizer
* **Runtime:** Node.js 8.10
* **Permissions**: choose or create a role with basic Lambda permissions (if you need help creating a role see below)

### Create Bundle
click Create function

You can create the bundle using `npm run zip`. This creates a oauth2-jwt-authorizer.zip deployment package in the `dist` folder with all the source, configuration and node modules AWS Lambda needs.
On the main function screen:

### Create Lambda function
* **Code entry type:** Upload a file from Amazon S3
* **Amazon S3 link URL:** https://s3.us-east-2.amazonaws.com/tom-smith-okta/aws-lambda-authorizer/lambda-oauth2-jwt-authorizer.zip
* **Handler:** index.handler

From the [AWS Lambda console](https://console.aws.amazon.com/lambda/home#/create?step=2)
### Environment variables

* **Name:** oauth2-jwt-authorizer
* **Description:** OAuth2 Bearer JWT authorizer for API Gateway
* **Runtime:** Node.js 4.3
* **Code entry type:** Upload a .ZIP file
* **Upload:** *select `dist\lambda-oauth2-jwt-authorizer.zip` we created in the previous step*
* **Handler:** index.handler
* **Role:** *select an existing role with `lambda:InvokeFunction` action*
Enter the following environment variables on the main function screen:

> If you don't have an existing role, you will need to create a new role as outlined below
ISSUER -> from your Okta authorization server

* **Memory (MB):** 128
* **Timeout:** 30 seconds
* **VPC:** No VPC
AUDIENCE -> from your Okta authorization server

Click **Next** and **Create**
CLIENT_ID -> a client_id from your Okta tenant

### Create IAM Role
The `audience` value should uniquely identify your AWS API Gateway deployment. You should assign unique audiences for each API Gateway authorizer instance so that a token intended for one gateway is not valid for another.

You will need to create an IAM Role that has permissions to invoke the Lambda function we created above.
Click **Save** to create your function.

### Creating an IAM Role for the Lambda function

If you don't already have an IAM role with permissions to create a Lambda function, you will need to create an IAM Role.

That Role will need to have a Policy similar to the following:

Expand All @@ -85,23 +83,23 @@ That Role will need to have a Policy similar to the following:
}
```

### Configure API Gateway
## Add the Lambda Authorizer to API Gateway

From the [AWS API Gateway console](https://console.aws.amazon.com/apigateway/home)

Open your API, or Create a new one.

In the left panel, under your API name, click on **Custom Authorizers**. Click on **Create**
In the left panel, under your API name, click on **Authorizers**. Click on **Create New Authorizer**

* **Name:** oauth2-jwt-authorizer
* **Lambda region:** *from previous step*
* **Execution role:** *the ARN of the Role we created in the previous step*
* **Identity token source:** `method.request.header.Authorization`
* **Type:** Lambda
* **Lambda function:** choose your new Lambda function
* **Lambda Invoke Role:** the ARN of the Role we created in the previous step
* **Lambda Event Payload:** Token
* **Token source:** Authorization
* **Token validation expression:** `^Bearer [-0-9a-zA-z\.]*$`

> Cut-and-paste this regular expression from ^ to $ inclusive

* **Result TTL in seconds:** 300
> Copy-and-paste this regular expression from ^ to $ inclusive

Click **Create**

Expand Down Expand Up @@ -132,14 +130,16 @@ A successful test will look something like:
]
}

### Configure API Gateway Methods to use the Authorizer
## Configure API Gateway Methods to use the Authorizer

In the left panel, under your API name, click on **Resources**.
Under the Resource tree, select one of your Methods (POST, GET etc.)

Select **Method Request**. Under **Authorization Settings** change:
Select **Method Request**. Under **Settings** change:

* Authorization: oauth2-jwt-authorizer

* Authorizer : oauth2-jwt-authorizer
Note: if you don't see your new Lambda function in the drop-down, refresh the page.

Make sure that:

Expand All @@ -151,7 +151,7 @@ Click the tick to save the changes.

You need to Deploy the API to make the changes public.

Select **Action** and **Deploy API**. Select your **Stage**.
Select **Actions** and **Deploy API**. Select your **Stage**.

### Test your endpoint remotely

Expand All @@ -173,3 +173,23 @@ You can use Postman to test the REST API
#### In (modern) browsers console with fetch

fetch( '<url>', { method: 'POST', headers: { Authorization : 'Bearer <token>' }}).then(response => { console.log( response );});

# Customizing the Authorizer

This sample currently enforces scope-based access to API resources using the `scp` claim in the JWT. The `api:read` scope is required for `GET` requests and `api:write` scope for `POST`, `PUT`, `PATCH`, or `DELETE` requests.

To add your own scopes and policies, update `index.js` with your authorization requirements.

## Manual Deployment

### Install Dependencies

Run `npm install` to download all of the authorizer's dependent modules. This is a prerequisite for deployment as AWS Lambda requires these files to be included in the uploaded bundle.

### Create Bundle

You can create the bundle using `npm run zip`. This creates a oauth2-jwt-authorizer.zip deployment package in the `dist` folder with all the source, configuration and node modules AWS Lambda needs.

### Create Lambda function

Follow the instructions above to create the Lambda function. You can either upload the new .zip file directly or upload it to an S3 bucket.
63 changes: 41 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
/******************************************************/
// Okta lambda authorizer for Amazon API Gateway

require('dotenv').config();

const JwtTokenHandler = require('oauth2-bearer-jwt-handler').JwtTokenHandler;
const AuthPolicy = require('./auth-policy');
const fs = require('fs');
const jwtTokenHandler = new JwtTokenHandler({
issuer: process.env.ISSUER,
audience: process.env.AUDIENCE,
jwks: fs.readFileSync('keys.json', 'utf8'),
const OktaJwtVerifier = require('@okta/jwt-verifier');

/******************************************************/

const oktaJwtVerifier = new OktaJwtVerifier({
issuer: process.env.ISSUER, // required
clientId: process.env.CLIENT_ID, // required
assertClaims: {
aud: process.env.AUDIENCE
}
});

const AuthPolicy = require('./auth-policy');

/******************************************************/

exports.handler = function(event, context) {
jwtTokenHandler.verifyRequest({
headers: {
authorization: event.authorizationToken
}
}, function(err, claims) {
if (err) {
console.log('Failed to validate bearer token', err);
return context.fail('Unauthorized');
}

var arr = event.authorizationToken.split(" ");

var access_token = arr[1];

oktaJwtVerifier.verifyAccessToken(access_token)
.then(jwt => {
// the token is valid (per definition of 'valid' above)
console.log(jwt.claims);

var claims = jwt.claims;

console.log('request principal: ' + claims);

Expand All @@ -30,32 +42,39 @@ exports.handler = function(event, context) {
apiOptions.restApiId = apiGatewayArnPart[0];
apiOptions.stage = apiGatewayArnPart[1];
const method = apiGatewayArnPart[2];
const resource = '/'; // root resource
var resource = '/'; // root resource

if (apiGatewayArnPart[3]) {
resource += apiGatewayArnPart[3];
}

const policy = new AuthPolicy(claims.sub, awsAccountId, apiOptions);

var policy = new AuthPolicy(claims.sub, awsAccountId, apiOptions);

/*
example scope based authorization

to allow full access:
policy.allowAllMethods()
*/
if (claims.hasScopes('api:read')) {
*/

if (claims.scp.includes('api:read')) {
policy.allowMethod(AuthPolicy.HttpVerb.GET, "*");
} else if (claims.hasScopes('api:write')) {
}
else if (claims.scp.includes('api:write')) {
policy.allowMethod(AuthPolicy.HttpVerb.POST, "*");
policy.allowMethod(AuthPolicy.HttpVerb.PUT, "*");
policy.allowMethod(AuthPolicy.HttpVerb.PATCH, "*");
policy.allowMethod(AuthPolicy.HttpVerb.DELETE, "*");
}

policy.allowMethod(AuthPolicy.HttpVerb.HEAD, "*");
policy.allowMethod(AuthPolicy.HttpVerb.OPTIONS, "*");

return context.succeed(policy.build());
})
.catch(err => {

console.log(err)
return context.fail('Unauthorized');
});
}
10 changes: 0 additions & 10 deletions keys.json

This file was deleted.

12 changes: 2 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,7 @@
},
"homepage": "https://github.com/mcguinness/node-lambda-oauth2-jwt-authorizer.git#readme",
"dependencies": {
"dotenv": "^2.0.0",
"oauth2-bearer-jwt-handler": "https://github.com/mcguinness/node-oauth2-bearer-jwt-handler.git"
},
"devDependencies": {
"lambda-local": "^1.1.0",
"grunt": "^1.0.1",
"grunt-aws": "^0.6.1",
"grunt-aws-lambda": "^0.13.0",
"grunt-env": "^0.4.4",
"load-grunt-tasks": "^3.4.0"
"@okta/jwt-verifier": "0.0.14",
"dotenv": "^7.0.0"
}
}