Skip to content

Instantly share code, notes, and snippets.

@danielrbradley
Created September 16, 2021 19:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielrbradley/49142ff7932bfa3d0ec6f17a076e834e to your computer and use it in GitHub Desktop.
Save danielrbradley/49142ff7932bfa3d0ec6f17a076e834e to your computer and use it in GitHub Desktop.
AWS API Gateway, JWT Authorizer with Cognito and CORS support
/**
* 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