Skip to content

Commit c4f3f87

Browse files
authored
feat(auth): add support for API Gateway Authorizers (#546)
Includes implementation, examples, tests, and documentation for Lambda and Cognito Authorizers
1 parent 30f210c commit c4f3f87

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6030
-24
lines changed

DEVELOPMENT_GUIDE.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,23 @@ Install snakeviz `pip install snakeviz`
8181
```
8282
python -m cProfile -o sam_profile_results bin/sam-translate.py translate --input-file=tests/translator/input/alexa_skill.yaml --output-file=cfn-template.json
8383
snakeviz sam_profile_results
84+
```
85+
86+
Verifying transforms
87+
--------------------
88+
89+
If you make changes to the transformer and want to verify the resulting CloudFormation template works as expected, you can transform your SAM template into a CloudFormation template using the following process:
90+
91+
```shell
92+
# Optional: You only need to run the package command in certain cases; e.g. when your CodeUri specifies a local path
93+
# Replace MY_TEMPLATE_PATH with the path to your template and MY_S3_BUCKET with an existing S3 bucket
94+
aws cloudformation package --template-file MY_TEMPLATE_PATH/template.yaml --output-template-file output-template.yaml --s3-bucket MY_S3_BUCKET
95+
96+
# Transform your SAM template into a CloudFormation template
97+
# Replace "output-template.yaml" if you didn't run the package command above or specified a different path for --output-template-file
98+
bin/sam-translate.py --input-file=output-template.yaml
99+
100+
# Deploy your transformed CloudFormation template
101+
# Replace MY_STACK_NAME with a unique name each time you deploy
102+
aws cloudformation deploy --template-file cfn-template.json --capabilities CAPABILITY_NAMED_IAM --stack-name MY_STACK_NAME
84103
```

bin/sam-translate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def main():
5757
except InvalidDocumentException as e:
5858
errorMessage = reduce(lambda message, error: message + ' ' + error.message, e.causes, e.message)
5959
print(errorMessage)
60-
errors = map(lambda cause: {'errorMessage': cause.message}, e.causes)
60+
errors = map(lambda cause: cause.message, e.causes)
6161
print(errors)
6262

6363

examples/.eslintrc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
extends: standard
2+
rules:
3+
prefer-promise-reject-errors: off # API Gateway expects string response from Lamdba (when using async + Promise.reject)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# API Gateway + Cognito Auth + Cognito Hosted Auth Example
2+
3+
This example shows you how to create an API Gateway API with a Cognito Authorizer using SAM.
4+
5+
## Running the example
6+
7+
Install the Node.js/NPM dependencies for your API's Lambda logic into the `src/` directory. This is necessary so that the dependencies get packaged up along with your Lambda function.
8+
9+
```bash
10+
npm install . --prefix ./src
11+
```
12+
13+
Deploy the example into your account (replace `YOUR_S3_ARTIFACTS_BUCKET` with an existing S3 bucket to store your app assets):
14+
15+
```bash
16+
# The following default values are also allowed: STACK_NAME, COGNITO_USER_POOL_CLIENT_NAME, COGNITO_USER_POOL_DOMAIN_PREFIX
17+
S3_BUCKET_NAME=YOUR_S3_ARTIFACTS_BUCKET \
18+
npm run package-deploy
19+
```
20+
21+
Cognito User Pools doesn't currently have CloudFormation support for configuring their Hosted Register/Signin UI. For now we will create these via the AWS CLI:
22+
23+
```bash
24+
npm run configure-cognito-user-pool
25+
```
26+
27+
Open the registration page created and hosted for you by Cognito in your browser. After the page loads, enter a Username and Password and click the Sign Up button.
28+
29+
```bash
30+
npm run open-signup-page
31+
32+
# Alternatively, you can open the login page by running `npm run open-login-page`
33+
```
34+
35+
After clicking Sign Up, you will be redirected to the UI client for your API.
36+
37+
To access the API UI directly as an unauthorized user (who has access to `GET /users` and `GET /users/{userId}`) you can run `npm run open-api-ui`.
38+
39+
## Additional resources
40+
41+
- https://aws.amazon.com/blogs/aws/launch-amazon-cognito-user-pools-general-availability-app-integration-and-federation/
42+
- https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
43+
- https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html
44+
- https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-invoke-api-integrated-with-cognito-user-pool.html
45+
- https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "api_cognito_auth",
3+
"version": "1.0.0",
4+
"description": "Example using API Gateway with Cognito Authorizer.",
5+
"main": "lambda.js",
6+
"license": "Apache-2.0",
7+
"dependencies": {
8+
"aws-serverless-express": "^3.3.3",
9+
"body-parser": "^1.17.1",
10+
"cors": "^2.8.3",
11+
"express": "^4.15.2",
12+
"pug": "^2.0.0-rc.1"
13+
},
14+
"scripts": {
15+
"package-deploy": "npm run set-config && npm run package && npm run deploy",
16+
"set-config": "npm config set STACK_NAME ${STACK_NAME:-sam-example-api-cognito-auth}",
17+
"package": "aws cloudformation package --template-file template.yaml --output-template-file template.packaged.yaml --s3-bucket $S3_BUCKET_NAME",
18+
"deploy": "aws cloudformation deploy --template-file ./template.packaged.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM",
19+
"configure-cognito-user-pool": "npm run set-cognito-user-pool-id && npm run set-cognito-user-pool-client-id && npm run set-api-id && npm run set-api-url && npm run update-user-pool-client && npm run create-user-pool-domain",
20+
"set-cognito-user-pool-id": "npm config set COGNITO_USER_POOL_ID $(aws cloudformation describe-stacks --stack-name $(npm config get STACK_NAME) --query 'Stacks[].Outputs[?OutputKey==`CognitoUserPoolId`].OutputValue' --output text)",
21+
"set-cognito-user-pool-client-id": "npm config set COGNITO_USER_POOL_CLIENT_ID $(aws cloudformation describe-stacks --stack-name $(npm config get STACK_NAME) --query 'Stacks[].Outputs[?OutputKey==`CognitoUserPoolClientId`].OutputValue' --output text)",
22+
"set-api-url": "npm config set API_URL $(aws cloudformation describe-stacks --stack-name sam-example-api-cognito-auth --query 'Stacks[].Outputs[?OutputKey==`ApiUrl`].OutputValue' --output text)",
23+
"set-api-id": "npm config set API_ID $(aws cloudformation describe-stacks --stack-name sam-example-api-cognito-auth --query 'Stacks[].Outputs[?OutputKey==`ApiId`].OutputValue' --output text)",
24+
"update-user-pool-client": "aws cognito-idp update-user-pool-client --user-pool-id $(npm config get COGNITO_USER_POOL_ID) --client-id $(npm config get COGNITO_USER_POOL_CLIENT_ID) --supported-identity-providers COGNITO --callback-urls \"[\\\"$(npm config get API_URL)\\\"]\" --allowed-o-auth-flows code implicit --allowed-o-auth-scopes openid email --allowed-o-auth-flows-user-pool-client",
25+
"create-user-pool-domain": "aws cognito-idp create-user-pool-domain --domain $(npm config get API_ID) --user-pool-id $(npm config get COGNITO_USER_POOL_ID)",
26+
"open-signup-page": "open \"https://$(npm config get API_ID).auth.us-east-1.amazoncognito.com/signup?response_type=code&client_id=$(npm config get COGNITO_USER_POOL_CLIENT_ID)&redirect_uri=$(npm config get API_URL)\"",
27+
"open-login-page": "open \"https://$(npm config get API_ID).auth.us-east-1.amazoncognito.com/login?response_type=code&client_id=$(npm config get COGNITO_USER_POOL_CLIENT_ID)&redirect_uri=$(npm config get API_URL)\"",
28+
"open-api-ui": "open \"$(npm config get API_URL)\""
29+
}
30+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict'
2+
const express = require('express')
3+
const bodyParser = require('body-parser')
4+
const cors = require('cors')
5+
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
6+
const app = express()
7+
const router = express.Router()
8+
9+
app.set('view engine', 'pug')
10+
11+
router.use(cors())
12+
router.use(bodyParser.json())
13+
router.use(bodyParser.urlencoded({ extended: true }))
14+
router.use(awsServerlessExpressMiddleware.eventContext())
15+
16+
router.get('/', (req, res) => {
17+
res.render('index', {
18+
apiId: req.apiGateway ? req.apiGateway.event.requestContext.apiId : null,
19+
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}` : 'http://localhost:3000',
20+
cognitoUserPoolClientId: process.env.COGNITO_USER_POOL_CLIENT_ID
21+
})
22+
})
23+
24+
router.get('/users', (req, res) => {
25+
res.json(users)
26+
})
27+
28+
router.get('/users/:userId', (req, res) => {
29+
const user = getUser(req.params.userId)
30+
31+
if (!user) return res.status(404).json({})
32+
33+
return res.json(user)
34+
})
35+
36+
router.post('/users', (req, res) => {
37+
const user = {
38+
id: ++userIdCounter,
39+
name: req.body.name
40+
}
41+
users.push(user)
42+
res.status(201).json(user)
43+
})
44+
45+
router.put('/users/:userId', (req, res) => {
46+
const user = getUser(req.params.userId)
47+
48+
if (!user) return res.status(404).json({})
49+
50+
user.name = req.body.name
51+
res.json(user)
52+
})
53+
54+
router.delete('/users/:userId', (req, res) => {
55+
const userIndex = getUserIndex(req.params.userId)
56+
57+
if (userIndex === -1) return res.status(404).json({})
58+
59+
users.splice(userIndex, 1)
60+
res.json(users)
61+
})
62+
63+
const getUser = (userId) => users.find(u => u.id === parseInt(userId))
64+
const getUserIndex = (userId) => users.findIndex(u => u.id === parseInt(userId))
65+
66+
// Ephemeral in-memory data store
67+
const users = [{
68+
id: 1,
69+
name: 'Joe'
70+
}, {
71+
id: 2,
72+
name: 'Jane'
73+
}]
74+
let userIdCounter = users.length
75+
76+
app.use('/', router)
77+
78+
// Export your express server so you can import it in the lambda function.
79+
module.exports = app
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict'
2+
const awsServerlessExpress = require('aws-serverless-express')
3+
const app = require('./app')
4+
5+
const server = awsServerlessExpress.createServer(app)
6+
7+
exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
doctype html
2+
html
3+
head
4+
title My Serverless Application
5+
style.
6+
body {
7+
width: 650px;
8+
margin: auto;
9+
}
10+
h1 {
11+
text-align: center;
12+
}
13+
.response > code {
14+
display: block;
15+
background-color: #eff0f1;
16+
color: #393318;
17+
padding: 5px;
18+
font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
19+
white-space: pre;
20+
overflow-x: auto;
21+
}
22+
form {
23+
margin-bottom: 1rem;
24+
}
25+
.form-group {
26+
padding-bottom: 1rem;
27+
}
28+
label {
29+
display: block;
30+
}
31+
body
32+
h1 My Serverless Application
33+
p
34+
| Public endpoints: GET /, GET /users, GET /users/{userId}
35+
p
36+
| Authorized endpoints: POST /users, PUT /users/{userId}, DELETE /users/{userId}
37+
38+
section.form
39+
h2 Invoke API
40+
p Experiment with the `users` resource with the form below.
41+
form
42+
div.form-group
43+
label(for='methodField') Method
44+
select(name='method' id='methodField')
45+
option(value='GET') GET
46+
option(value='POST') POST
47+
option(value='PUT') PUT
48+
option(value='DELETE') DELETE
49+
div.form-group
50+
label(for='idField') user id
51+
input(type='text' name='id' id='idField')
52+
div.form-group
53+
label(for='nameField') name
54+
input(type='text' name='name' id='nameField')
55+
input(type='submit')
56+
57+
section
58+
h2 Response
59+
p.request
60+
span.request__method GET
61+
span  
62+
spand.request__endpoint /users
63+
section.response
64+
code
65+
66+
script.
67+
68+
const apiId = '#{apiId}'
69+
const apiUrl = '#{apiUrl}/'
70+
const cognitoUserPoolClientId = '#{cognitoUserPoolClientId}'
71+
72+
const queryStringParams = new URLSearchParams(window.location.search)
73+
const cognitoCode = queryStringParams.get('code')
74+
let cognitoIdentityToken = localStorage.getItem('cognitoIdentityToken')
75+
76+
const form = document.querySelector('form')
77+
form.addEventListener('submit', onApiInvokeFormSubmit)
78+
79+
fetch('users')
80+
.then(setResponseText)
81+
.catch(setResponseText)
82+
83+
if (cognitoCode) {
84+
exchangeCodeForAccessToken()
85+
.then(response => response.json())
86+
.then(json => {
87+
if (json.id_token) {
88+
cognitoIdentityToken = json.id_token
89+
localStorage.setItem('cognitoIdentityToken', cognitoIdentityToken)
90+
}
91+
})
92+
}
93+
94+
function convertJsonToFormUrlEncoded(json) {
95+
const oAuthTokenBodyArray = Object.entries(json).map(([key, value]) => {
96+
const encodedKey = encodeURIComponent(key)
97+
const encodedValue = encodeURIComponent(value)
98+
99+
return `${encodedKey}=${encodedValue}`
100+
})
101+
102+
return oAuthTokenBodyArray.join('&')
103+
}
104+
105+
function exchangeCodeForAccessToken() {
106+
const oauthTokenBodyJson = {
107+
grant_type: 'authorization_code',
108+
client_id: cognitoUserPoolClientId,
109+
code: cognitoCode,
110+
redirect_uri: apiUrl
111+
}
112+
const oauthTokenBody = convertJsonToFormUrlEncoded(oauthTokenBodyJson)
113+
114+
return fetch(`https://${apiId}.auth.us-east-1.amazoncognito.com/oauth2/token`,
115+
{
116+
method: 'POST',
117+
headers: {
118+
['Content-Type']: 'application/x-www-form-urlencoded'
119+
},
120+
body: oauthTokenBody
121+
})
122+
}
123+
124+
function onApiInvokeFormSubmit (event) {
125+
event.preventDefault()
126+
const method = document.getElementById('methodField').value
127+
const id = document.getElementById('idField').value
128+
const name = document.getElementById('nameField').value
129+
const endpoint = id ? 'users/' + id : 'users'
130+
const body = ['POST', 'PUT'].includes(method) ? JSON.stringify({ name: name }) : undefined
131+
const headers = {
132+
'content-type': 'application/json',
133+
'Authorization': cognitoIdentityToken
134+
}
135+
136+
document.querySelector('.request__method').innerText = method
137+
document.querySelector('.request__endpoint').innerText = `/${endpoint}`
138+
139+
return fetch(endpoint, {
140+
method,
141+
headers,
142+
body
143+
})
144+
.then(setResponseText)
145+
.catch(setResponseText)
146+
}
147+
148+
function setResponseText(response) {
149+
const contentType = response.headers.get('content-type')
150+
if (contentType.includes('application/json')) {
151+
return response.json().then(json => {
152+
document.querySelector('code').innerText = JSON.stringify(json, null, 4)
153+
})
154+
}
155+
156+
return response.text().then(text => {
157+
document.querySelector('code').innerText = text
158+
})
159+
}

0 commit comments

Comments
 (0)