Created
September 16, 2021 19:53
-
-
Save danielrbradley/49142ff7932bfa3d0ec6f17a076e834e to your computer and use it in GitHub Desktop.
AWS API Gateway, JWT Authorizer with Cognito and CORS support
This file contains hidden or 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
/** | |
* Annotated, real-world example of how to use | |
* AWS API Gateway V2 + JWT authorizer + Cognito | |
* with support for CORS & good security practices | |
* deployed with Pulumi (https://www.pulumi.com/) | |
*/ | |
import * as pulumi from '@pulumi/pulumi'; | |
import * as aws from '@pulumi/aws'; | |
import serverlessExpress from '@vendia/serverless-express'; | |
import express from 'express'; | |
import cors from 'cors'; | |
import helmet from 'helmet'; | |
import noCache from 'nocache'; | |
// Cognito UserPool | |
const userPool = new aws.cognito.UserPool(`user-pool`, { | |
schemas: [ | |
{ | |
name: 'email', | |
attributeDataType: 'String', | |
mutable: true, | |
required: true, | |
stringAttributeConstraints: { | |
maxLength: '128', | |
minLength: '3', | |
}, | |
}, | |
{ | |
name: 'phone_number', | |
attributeDataType: 'String', | |
mutable: true, | |
required: false, | |
}, | |
], | |
// NIST recommended password policy | |
passwordPolicy: { | |
minimumLength: 8, | |
requireUppercase: false, | |
requireLowercase: false, | |
requireNumbers: false, | |
requireSymbols: false, | |
temporaryPasswordValidityDays: 7, | |
}, | |
adminCreateUserConfig: { | |
// Useful for internal applications - just allow admins to create users. | |
allowAdminCreateUserOnly: true, | |
}, | |
usernameConfiguration: { | |
// If your username is an email then this stops much confusion | |
// over how they originally typed their email address. | |
caseSensitive: false, | |
}, | |
usernameAttributes: ['email'], | |
mfaConfiguration: 'OPTIONAL', | |
// Give option to remember devices to reduce MFA frequency. | |
deviceConfiguration: { | |
challengeRequiredOnNewDevice: true, | |
deviceOnlyRememberedOnUserPrompt: true, | |
}, | |
accountRecoverySetting: { | |
recoveryMechanisms: [{ name: 'verified_email', priority: 1 }], | |
}, | |
userPoolAddOns: { | |
// Enable dynamic MFA requirement & compromised password checking | |
advancedSecurityMode: 'ENFORCED', | |
}, | |
}); | |
const userPoolClient = new aws.cognito.UserPoolClient(`user-pool-client`, { | |
userPoolId: userPool.id, | |
// Don't indicate if an account exists or not | |
preventUserExistenceErrors: 'ENABLED', | |
// Allow revoking of refresh tokens on logout to prevent future token issuing. | |
enableTokenRevocation: true, | |
}); | |
const lambdaRole = new aws.iam.Role(`api-role`, { | |
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ | |
Service: 'lambda.amazonaws.com', | |
}), | |
}); | |
new aws.iam.RolePolicy(`api-role-policy`, { | |
role: lambdaRole, | |
policy: { | |
Version: '2012-10-17', | |
Statement: [ | |
{ | |
Action: [ | |
'logs:CreateLogGroup', | |
'logs:CreateLogStream', | |
'logs:PutLogEvents', | |
'ec2:CreateNetworkInterface', | |
'ec2:DescribeNetworkInterfaces', | |
'ec2:DeleteNetworkInterface', | |
], | |
Resource: '*', | |
Effect: 'Allow', | |
}, | |
{ | |
// Allow the API to manage users. | |
// Useful for internal applications where users can't sign up themselves. | |
Action: [ | |
'cognito-idp:AdminCreateUser', | |
'cognito-idp:AdminGet*', | |
'cognito-idp:AdminList*', | |
'cognito-idp:Describe*', | |
'cognito-idp:Get*', | |
'cognito-idp:List*', | |
], | |
Resource: userPool.arn, | |
Effect: 'Allow', | |
}, | |
], | |
}, | |
}); | |
const apiFunction = new aws.lambda.CallbackFunction(`api-function`, { | |
runtime: 'nodejs14.x', | |
role: lambdaRole, | |
callbackFactory: () => { | |
const app = express(); | |
// Helmet applies lots of recommended security-related headers: | |
// - contentSecurityPolicy | |
// - dnsPrefetchControl | |
// - expectCt | |
// - frameguard | |
// - hidePoweredBy | |
// - hsts | |
// - ieNoOpen | |
// - noSniff | |
// - permittedCrossDomainPolicies | |
// - referrerPolicy | |
// - xssFilter | |
app.use(helmet()); | |
// API responses rarely want to cached | |
app.use(noCache()); | |
// Intercepts CORS preflight OPTIONS requests | |
app.use(cors()); | |
app.use(express.json()); | |
app.use((req, res, next) => { | |
// Example of accessing claims from the API Gateway JWT authorizer | |
const { event } = serverlessExpress.getCurrentInvoke(); | |
res.locals.currentUserEmail = event.requestContext?.authorizer?.jwt?.claims?.email; | |
next(); | |
}); | |
app.get('/v1/test', (req, res) => { | |
res.json({ | |
message: 'Hello world!', | |
}); | |
}); | |
return serverlessExpress({ app }); | |
}, | |
}); | |
// Always create the log group up front so we can manage retention or monitor errors in the future. | |
new aws.cloudwatch.LogGroup(`api-log-group`, { | |
name: pulumi.interpolate`/aws/lambda/${apiFunction.name}`, | |
retentionInDays: 365, | |
}); | |
// API Gateway | |
const apiGateway = new aws.apigatewayv2.Api(`api-gateway`, { | |
protocolType: 'HTTP', | |
// API Gateway will call the handler for the rest of the response, | |
// then just set the specified CORS headers. | |
corsConfiguration: { | |
allowMethods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], | |
allowHeaders: ['Authorization', 'Content-type'], | |
}, | |
}); | |
// Allow our API Gateway to invoke our lambda function | |
new aws.lambda.Permission(`lambdaPermission`, { | |
action: 'lambda:InvokeFunction', | |
principal: 'apigateway.amazonaws.com', | |
function: apiFunction, | |
sourceArn: pulumi.interpolate`${apiGateway.executionArn}/*/*`, | |
}); | |
// Connect API Gateway --> Lambda | |
const integration = new aws.apigatewayv2.Integration(`api-integration`, { | |
apiId: apiGateway.id, | |
integrationType: 'AWS_PROXY', | |
integrationUri: apiFunction.arn, | |
requestParameters: { | |
'overwrite:path': '$request.path', | |
}, | |
integrationMethod: 'ANY', | |
payloadFormatVersion: '2.0', | |
passthroughBehavior: 'WHEN_NO_MATCH', | |
}); | |
const cognitoAuthorizer = new aws.apigatewayv2.Authorizer(`api-cognito-authorizer`, { | |
apiId: apiGateway.id, | |
name: 'Cognito', | |
authorizerType: 'JWT', | |
identitySources: [`$request.header.Authorization`], | |
jwtConfiguration: { | |
// To work with Cognito's JWTs, the issuer is the user pool | |
issuer: pulumi.interpolate`https://${userPool.endpoint}`, | |
// and the audience is our client. | |
audiences: [userPoolClient.id], | |
}, | |
}); | |
// If no other routes match, then authorize and invoke our handler | |
const defaultRoute = new aws.apigatewayv2.Route(`api-default-route`, { | |
apiId: apiGateway.id, | |
routeKey: '$default', | |
target: pulumi.interpolate`integrations/${integration.id}`, | |
authorizerId: cognitoAuthorizer.id, | |
authorizationType: 'JWT', | |
}); | |
// Don't authorize CORS preflight requests | |
const optionsRoute = new aws.apigatewayv2.Route(`api-options-route`, { | |
apiId: apiGateway.id, | |
routeKey: 'OPTIONS /{proxy+}', | |
target: pulumi.interpolate`integrations/${integration.id}`, | |
}); | |
new aws.apigatewayv2.Stage( | |
`api-stage`, | |
{ | |
apiId: apiGateway.id, | |
name: 'default', | |
autoDeploy: true, | |
}, | |
{ dependsOn: [defaultRoute, optionsRoute] } | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment