Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add support for API Gateway Authorizers #546

Merged
merged 20 commits into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd28c89
feat(auth): add support for API Gateway Authorizers
brettstack Aug 9, 2018
8cb9f63
addressed my own feedback in GitHub comments
brettstack Aug 9, 2018
915760c
improvement: remove check for Auth dict since it's verified by proper…
brettstack Aug 10, 2018
0ea0ce1
test: add error tests for authorizers
brettstack Aug 10, 2018
f4ecc38
test: fix missing DefaultAuthorizer in Authorizers test
brettstack Aug 10, 2018
e451739
fix: add permissions for API to invoke Authorizer Lambda Function
brettstack Aug 15, 2018
18ecefc
fix: fix error when no Auth defined
brettstack Aug 15, 2018
ea7efc8
docs: add Lambda REQUEST Authorizer example
brettstack Aug 16, 2018
e0b412f
fix: add error handling when no identity source provided for Lambda R…
brettstack Aug 17, 2018
da1867e
docs(examples): improve README commands for api_lambda_request_auth
brettstack Aug 24, 2018
3998447
add API Gateway + Cognito Authorizer example
brettstack Sep 7, 2018
60671cc
simplify setup by using npm scripts
brettstack Sep 11, 2018
e9d6c25
update to use authorization_code flow
brettstack Sep 13, 2018
9872796
add tests for cognito authorizers
brettstack Sep 14, 2018
1d1be1c
merge upstream/develop
brettstack Sep 17, 2018
90b3a39
convert result of map() to list for py3 support
brettstack Sep 17, 2018
6dca4c7
update spec to include API Auth and Function Auth
brettstack Sep 18, 2018
c28ae1a
add TOC for Data Types in spec
brettstack Sep 18, 2018
847646d
address documentation change requests
brettstack Sep 20, 2018
f6ac94b
address PR change requests
brettstack Sep 21, 2018
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
19 changes: 19 additions & 0 deletions DEVELOPMENT_GUIDE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,23 @@ Install snakeviz `pip install snakeviz`
```
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
snakeviz sam_profile_results
```

Verifying transforms
--------------------

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:

```shell
# Optional: You only need to run the package command in certain cases; e.g. when your CodeUri specifies a local path
# Replace MY_TEMPLATE_PATH with the path to your template and MY_S3_BUCKET with an existing S3 bucket
aws cloudformation package --template-file MY_TEMPLATE_PATH/template.yaml --output-template-file output-template.yaml --s3-bucket MY_S3_BUCKET

# Transform your SAM template into a CloudFormation template
# Replace "output-template.yaml" if you didn't run the package command above or specified a different path for --output-template-file
bin/sam-translate.py --input-file=output-template.yaml

# Deploy your transformed CloudFormation template
# Replace MY_STACK_NAME with a unique name each time you deploy
aws cloudformation deploy --template-file cfn-template.json --capabilities CAPABILITY_NAMED_IAM --stack-name MY_STACK_NAME
```
2 changes: 1 addition & 1 deletion bin/sam-translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def main():
except InvalidDocumentException as e:
errorMessage = reduce(lambda message, error: message + ' ' + error.message, e.causes, e.message)
print(errorMessage)
errors = map(lambda cause: {'errorMessage': cause.message}, e.causes)
errors = map(lambda cause: cause.message, e.causes)
print(errors)


Expand Down
3 changes: 3 additions & 0 deletions examples/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
extends: standard
rules:
prefer-promise-reject-errors: off # API Gateway expects string response from Lamdba (when using async + Promise.reject)
45 changes: 45 additions & 0 deletions examples/2016-10-31/api_cognito_auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# API Gateway + Cognito Auth + Cognito Hosted Auth Example

This example shows you how to create an API Gateway API with a Cognito Authorizer using SAM.

## Running the example

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.

```bash
npm install . --prefix ./src
```

Deploy the example into your account (replace `YOUR_S3_ARTIFACTS_BUCKET` with an existing S3 bucket to store your app assets):

```bash
# The following default values are also allowed: STACK_NAME, COGNITO_USER_POOL_CLIENT_NAME, COGNITO_USER_POOL_DOMAIN_PREFIX
S3_BUCKET_NAME=YOUR_S3_ARTIFACTS_BUCKET \
npm run package-deploy
```

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:

```bash
npm run configure-cognito-user-pool
```

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.

```bash
npm run open-signup-page

# Alternatively, you can open the login page by running `npm run open-login-page`
```

After clicking Sign Up, you will be redirected to the UI client for your API.

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`.

## Additional resources

- https://aws.amazon.com/blogs/aws/launch-amazon-cognito-user-pools-general-availability-app-integration-and-federation/
- https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
- https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html
- https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-invoke-api-integrated-with-cognito-user-pool.html
- https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
30 changes: 30 additions & 0 deletions examples/2016-10-31/api_cognito_auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "api_cognito_auth",
"version": "1.0.0",
"description": "Example using API Gateway with Cognito Authorizer.",
"main": "lambda.js",
"license": "Apache-2.0",
"dependencies": {
"aws-serverless-express": "^3.3.3",
"body-parser": "^1.17.1",
"cors": "^2.8.3",
"express": "^4.15.2",
"pug": "^2.0.0-rc.1"
},
"scripts": {
"package-deploy": "npm run set-config && npm run package && npm run deploy",
"set-config": "npm config set STACK_NAME ${STACK_NAME:-sam-example-api-cognito-auth}",
"package": "aws cloudformation package --template-file template.yaml --output-template-file template.packaged.yaml --s3-bucket $S3_BUCKET_NAME",
"deploy": "aws cloudformation deploy --template-file ./template.packaged.yaml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM",
"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",
"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)",
"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)",
"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)",
"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)",
"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",
"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)",
"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)\"",
"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)\"",
"open-api-ui": "open \"$(npm config get API_URL)\""
}
}
79 changes: 79 additions & 0 deletions examples/2016-10-31/api_cognito_auth/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict'
const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
const app = express()
const router = express.Router()

app.set('view engine', 'pug')

router.use(cors())
router.use(bodyParser.json())
router.use(bodyParser.urlencoded({ extended: true }))
router.use(awsServerlessExpressMiddleware.eventContext())

router.get('/', (req, res) => {
res.render('index', {
apiId: req.apiGateway ? req.apiGateway.event.requestContext.apiId : null,
apiUrl: req.apiGateway ? `https://${req.apiGateway.event.headers.Host}/${req.apiGateway.event.requestContext.stage}` : 'http://localhost:3000',
cognitoUserPoolClientId: process.env.COGNITO_USER_POOL_CLIENT_ID
})
})

router.get('/users', (req, res) => {
res.json(users)
})

router.get('/users/:userId', (req, res) => {
const user = getUser(req.params.userId)

if (!user) return res.status(404).json({})

return res.json(user)
})

router.post('/users', (req, res) => {
const user = {
id: ++userIdCounter,
name: req.body.name
}
users.push(user)
res.status(201).json(user)
})

router.put('/users/:userId', (req, res) => {
const user = getUser(req.params.userId)

if (!user) return res.status(404).json({})

user.name = req.body.name
res.json(user)
})

router.delete('/users/:userId', (req, res) => {
const userIndex = getUserIndex(req.params.userId)

if (userIndex === -1) return res.status(404).json({})

users.splice(userIndex, 1)
res.json(users)
})

const getUser = (userId) => users.find(u => u.id === parseInt(userId))
const getUserIndex = (userId) => users.findIndex(u => u.id === parseInt(userId))

// Ephemeral in-memory data store
const users = [{
id: 1,
name: 'Joe'
}, {
id: 2,
name: 'Jane'
}]
let userIdCounter = users.length

app.use('/', router)

// Export your express server so you can import it in the lambda function.
module.exports = app
7 changes: 7 additions & 0 deletions examples/2016-10-31/api_cognito_auth/src/lambda.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'
const awsServerlessExpress = require('aws-serverless-express')
const app = require('./app')

const server = awsServerlessExpress.createServer(app)

exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context)
159 changes: 159 additions & 0 deletions examples/2016-10-31/api_cognito_auth/src/views/index.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
doctype html
html
head
title My Serverless Application
style.
body {
width: 650px;
margin: auto;
}
h1 {
text-align: center;
}
.response > code {
display: block;
background-color: #eff0f1;
color: #393318;
padding: 5px;
font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
white-space: pre;
overflow-x: auto;
}
form {
margin-bottom: 1rem;
}
.form-group {
padding-bottom: 1rem;
}
label {
display: block;
}
body
h1 My Serverless Application
p
| Public endpoints: GET /, GET /users, GET /users/{userId}
p
| Authorized endpoints: POST /users, PUT /users/{userId}, DELETE /users/{userId}

section.form
h2 Invoke API
p Experiment with the `users` resource with the form below.
form
div.form-group
label(for='methodField') Method
select(name='method' id='methodField')
option(value='GET') GET
option(value='POST') POST
option(value='PUT') PUT
option(value='DELETE') DELETE
div.form-group
label(for='idField') user id
input(type='text' name='id' id='idField')
div.form-group
label(for='nameField') name
input(type='text' name='name' id='nameField')
input(type='submit')

section
h2 Response
p.request
span.request__method GET
span  
spand.request__endpoint /users
section.response
code

script.

const apiId = '#{apiId}'
const apiUrl = '#{apiUrl}/'
const cognitoUserPoolClientId = '#{cognitoUserPoolClientId}'

const queryStringParams = new URLSearchParams(window.location.search)
const cognitoCode = queryStringParams.get('code')
let cognitoIdentityToken = localStorage.getItem('cognitoIdentityToken')

const form = document.querySelector('form')
form.addEventListener('submit', onApiInvokeFormSubmit)

fetch('users')
.then(setResponseText)
.catch(setResponseText)

if (cognitoCode) {
exchangeCodeForAccessToken()
.then(response => response.json())
.then(json => {
if (json.id_token) {
cognitoIdentityToken = json.id_token
localStorage.setItem('cognitoIdentityToken', cognitoIdentityToken)
}
})
}

function convertJsonToFormUrlEncoded(json) {
const oAuthTokenBodyArray = Object.entries(json).map(([key, value]) => {
const encodedKey = encodeURIComponent(key)
const encodedValue = encodeURIComponent(value)

return `${encodedKey}=${encodedValue}`
})

return oAuthTokenBodyArray.join('&')
}

function exchangeCodeForAccessToken() {
const oauthTokenBodyJson = {
grant_type: 'authorization_code',
client_id: cognitoUserPoolClientId,
code: cognitoCode,
redirect_uri: apiUrl
}
const oauthTokenBody = convertJsonToFormUrlEncoded(oauthTokenBodyJson)

return fetch(`https://${apiId}.auth.us-east-1.amazoncognito.com/oauth2/token`,
keetonian marked this conversation as resolved.
Show resolved Hide resolved
{
method: 'POST',
headers: {
['Content-Type']: 'application/x-www-form-urlencoded'
},
body: oauthTokenBody
})
}

function onApiInvokeFormSubmit (event) {
event.preventDefault()
const method = document.getElementById('methodField').value
const id = document.getElementById('idField').value
const name = document.getElementById('nameField').value
const endpoint = id ? 'users/' + id : 'users'
const body = ['POST', 'PUT'].includes(method) ? JSON.stringify({ name: name }) : undefined
const headers = {
'content-type': 'application/json',
'Authorization': cognitoIdentityToken
}

document.querySelector('.request__method').innerText = method
document.querySelector('.request__endpoint').innerText = `/${endpoint}`

return fetch(endpoint, {
method,
headers,
body
})
.then(setResponseText)
.catch(setResponseText)
}

function setResponseText(response) {
const contentType = response.headers.get('content-type')
if (contentType.includes('application/json')) {
return response.json().then(json => {
document.querySelector('code').innerText = JSON.stringify(json, null, 4)
})
}

return response.text().then(text => {
document.querySelector('code').innerText = text
})
}
Loading