forked from pulumi/examples
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial code example for custom authorizer w Auth0 (pulumi#282)
* code example for custom authorizer w Auth0
- Loading branch information
Showing
5 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
name: lambda-authorizer | ||
runtime: nodejs | ||
description: A sample AWS API Gateway example that uses Custom Authorizers |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) | ||
|
||
# Auth0 Protected Serverless REST API on AWS | ||
|
||
A simple REST API that is protected by a custom AWS Lambda Authorizer. The Authorizer uses [Auth0](https://auth0.com/) to authorize requests. | ||
|
||
This example is similar to Auth0's tutorial: [Secure AWS API Gateway Endpoints Using Custom Authorizers](https://auth0.com/docs/integrations/aws-api-gateway/custom-authorizers), but uses Pulumi to create the Serverless app and Custom Authorizer. | ||
|
||
## Set Up Auth0 | ||
|
||
You can follow the steps below or alternatively you can follow [Auth0's Part 1: Create an Auth0 API](https://auth0.com/docs/integrations/aws-api-gateway/custom-authorizers/part-1). | ||
|
||
1. [Sign up](https://auth0.com/signup) for an Auth0 account or login if you already have one. | ||
|
||
1. Click on `APIs` in the left-hand menu. | ||
|
||
1. Click `Create API`. | ||
|
||
* Enter a name and Identifier for you New API. | ||
* Select RS256 as the Signing Algorithm. | ||
* Click `Create`. | ||
|
||
1. Under the `Quick Start` tab, the Node.js example will show you the values for `jwksUri`, `audience` and `issuer` you will need in the next section. | ||
|
||
## Deploying and Running the Program | ||
|
||
1. Create a new stack: | ||
|
||
```bash | ||
pulumi stack init auth0-api-testing | ||
``` | ||
|
||
1. Set the AWS region: | ||
|
||
```bash | ||
pulumi config set aws:region us-east-2 | ||
``` | ||
|
||
1. Set up the Auth0 configuration values as secrets in Pulumi: | ||
|
||
Run the following commands after replacing `<jwksUri>`, `<audience>` and `<issuer>` with the appropriate values. | ||
|
||
```bash | ||
pulumi config set --secret jwksUri <jwksUri> | ||
pulumi config set --secret audience <audience> | ||
pulumi config set --secret issuer <issuer> | ||
``` | ||
|
||
1. Restore NPM modules via `npm install` or `yarn install`. | ||
|
||
1. Run `pulumi up` to preview and deploy changes: | ||
|
||
```bash | ||
$ pulumi up | ||
Previewing update (dev): | ||
... | ||
Updating (dev): | ||
Type Name Status Info | ||
+ pulumi:pulumi:Stack lambda-authorizer-dev created 1 message | ||
+ ├─ aws:apigateway:x:API myapi created | ||
+ │ ├─ aws:iam:Role myapi70a45a97 created | ||
+ │ ├─ aws:iam:RolePolicyAttachment myapi70a45a97-32be53a2 created | ||
+ │ ├─ aws:lambda:Function myapi70a45a97 created | ||
+ │ ├─ aws:apigateway:RestApi myapi created | ||
+ │ ├─ aws:apigateway:Deployment myapi created | ||
+ │ ├─ aws:lambda:Permission myapi-31a4e902 created | ||
+ │ └─ aws:apigateway:Stage myapi created | ||
+ ├─ aws:iam:Role jwt-rsa-custom-authorizer created | ||
+ ├─ aws:iam:Role jwt-rsa-custom-authorizer-authorizer-role created | ||
+ ├─ aws:iam:RolePolicyAttachment jwt-rsa-custom-authorizer-32be53a2 created | ||
+ ├─ aws:lambda:Function jwt-rsa-custom-authorizer created | ||
+ └─ aws:iam:RolePolicy jwt-rsa-custom-authorizer-invocation-policy created | ||
Outputs: | ||
url: "https://***.execute-api.us-east-2.amazonaws.com/stage/" | ||
Resources: | ||
+ 14 created | ||
Duration: 18s | ||
``` | ||
|
||
## Testing Our API | ||
|
||
We can now use cURL to test out our new endpoint. If we cURL without a token, we should get a 401 Unauthorized response. | ||
|
||
```bash | ||
$ curl $(pulumi stack output url)hello | ||
{"message":"Unauthorized"} | ||
``` | ||
|
||
We can curl our endpoint with an invalid token and should once again get a 401 Unauthorized response. | ||
|
||
```bash | ||
$ curl $(pulumi stack output url)hello -H "Authorization: Bearer invalid" | ||
{"message":"Unauthorized"} | ||
``` | ||
|
||
Finally, we expect a 200 response when we obtain a token from Auth0 and use it to call our API. We can get a token by visiting the API Details page for our API and clicking the Test tab. Using the provided access token and the API a 200 response: Hello world! | ||
|
||
```bash | ||
$ curl $(pulumi stack output url)hello -H "Authorization: Bearer <VALID_TOKEN>" | ||
<h1>Hello world!</h1> | ||
``` | ||
|
||
## Clean up | ||
|
||
1. Run `pulumi destroy` to tear down all resources. | ||
|
||
1. To delete the stack itself, run `pulumi stack rm`. Note that this command deletes all deployment history from the Pulumi Console. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// Copyright 2016-2019, Pulumi Corporation. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
import * as awsx from "@pulumi/awsx"; | ||
import * as pulumi from '@pulumi/pulumi'; | ||
|
||
import * as jwksClient from 'jwks-rsa'; | ||
import * as jwt from 'jsonwebtoken'; | ||
import * as util from 'util'; | ||
|
||
const config = new pulumi.Config(); | ||
const jwksUri = config.require("jwksUri"); | ||
const audience = config.require("audience"); | ||
const issuer = config.require("issuer"); | ||
|
||
const authorizerLambda = async (event: awsx.apigateway.AuthorizerEvent) => { | ||
try { | ||
return await authenticate(event); | ||
} | ||
catch (err) { | ||
console.log(err); | ||
// Tells API Gateway to return a 401 Unauthorized response | ||
throw new Error("Unauthorized"); | ||
} | ||
}; | ||
|
||
// Create our API and reference the Lambda authorizer | ||
const api = new awsx.apigateway.API("myapi", { | ||
routes: [{ | ||
path: "/hello", | ||
method: "GET", | ||
eventHandler: async () => { | ||
return { | ||
statusCode: 200, | ||
body: "<h1>Hello world!</h1>", | ||
}; | ||
}, | ||
authorizers: awsx.apigateway.getTokenLambdaAuthorizer({ | ||
authorizerName: "jwt-rsa-custom-authorizer", | ||
header: "Authorization", | ||
handler: authorizerLambda, | ||
identityValidationExpression: "^Bearer [-0-9a-zA-Z\._]*$", | ||
authorizerResultTtlInSeconds: 3600, | ||
}), | ||
}], | ||
}); | ||
|
||
// Export the URL for our API | ||
export const url = api.url; | ||
|
||
/** | ||
* Below is all code that gets added to the Authorizer Lambda. The code was copied and | ||
* converted to TypeScript from [Auth0's GitHub | ||
* Example](https://github.com/auth0-samples/jwt-rsa-aws-custom-authorizer) | ||
*/ | ||
|
||
// Extract and return the Bearer Token from the Lambda event parameters | ||
function getToken(event: awsx.apigateway.AuthorizerEvent): string { | ||
if (!event.type || event.type !== 'TOKEN') { | ||
throw new Error('Expected "event.type" parameter to have value "TOKEN"'); | ||
} | ||
|
||
const tokenString = event.authorizationToken; | ||
if (!tokenString) { | ||
throw new Error('Expected "event.authorizationToken" parameter to be set'); | ||
} | ||
|
||
const match = tokenString.match(/^Bearer (.*)$/); | ||
if (!match) { | ||
throw new Error(`Invalid Authorization token - ${tokenString} does not match "Bearer .*"`); | ||
} | ||
return match[1]; | ||
} | ||
|
||
// Check the Token is valid with Auth0 | ||
async function authenticate(event: awsx.apigateway.AuthorizerEvent): Promise<awsx.apigateway.AuthorizerResponse> { | ||
console.log(event); | ||
const token = getToken(event); | ||
|
||
const decoded = jwt.decode(token, { complete: true }); | ||
if (!decoded || typeof decoded === "string" || !decoded.header || !decoded.header.kid) { | ||
throw new Error('invalid token'); | ||
} | ||
|
||
const client = jwksClient({ | ||
cache: true, | ||
rateLimit: true, | ||
jwksRequestsPerMinute: 10, // Default value | ||
jwksUri: jwksUri | ||
}); | ||
|
||
const key = await util.promisify(client.getSigningKey)(decoded.header.kid); | ||
const signingKey = key.publicKey || key.rsaPublicKey; | ||
if (!signingKey) { | ||
throw new Error('could not get signing key'); | ||
} | ||
|
||
const verifiedJWT = await jwt.verify(token, signingKey, { audience, issuer }); | ||
if (!verifiedJWT || typeof verifiedJWT === "string" || !isVerifiedJWT(verifiedJWT)) { | ||
throw new Error('could not verify JWT'); | ||
} | ||
return awsx.apigateway.authorizerResponse(verifiedJWT.sub, 'Allow', event.methodArn); | ||
} | ||
|
||
interface VerifiedJWT { | ||
sub: string, | ||
} | ||
|
||
function isVerifiedJWT(toBeDetermined: VerifiedJWT | Object): toBeDetermined is VerifiedJWT { | ||
return (<VerifiedJWT>toBeDetermined).sub !== undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"name": "aws-typescript", | ||
"devDependencies": { | ||
"@types/node": "^11.13.6" | ||
}, | ||
"dependencies": { | ||
"@pulumi/aws": "latest", | ||
"@pulumi/awsx": "latest", | ||
"@pulumi/pulumi": "latest", | ||
"@types/auth0-js": "^9.10.1", | ||
"@types/jsonwebtoken": "^8.3.2", | ||
"jsonwebtoken": "^8.5.1", | ||
"jwks-rsa": "^1.4.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"compilerOptions": { | ||
"outDir": "bin", | ||
"target": "es6", | ||
"lib": [ | ||
"es6" | ||
], | ||
"module": "commonjs", | ||
"moduleResolution": "node", | ||
"sourceMap": true, | ||
"experimentalDecorators": true, | ||
"pretty": true, | ||
"noFallthroughCasesInSwitch": true, | ||
"noImplicitAny": true, | ||
"noImplicitReturns": true, | ||
"forceConsistentCasingInFileNames": true, | ||
"strictNullChecks": true | ||
}, | ||
"files": [ | ||
"index.ts" | ||
] | ||
} |