Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save alanzhaonys/b30ec8057817ae1ee2129684bd9a3d6c to your computer and use it in GitHub Desktop.
Save alanzhaonys/b30ec8057817ae1ee2129684bd9a3d6c to your computer and use it in GitHub Desktop.
Secure API Gateway with Cognito
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
AppName:
Type: String
Default: apigateway-cognito-jwt-authorizer
Description: The application name
VpcCidrBlock:
Type: String
Default: 10.0.0.0/16
Description: VPC CIDR block
ApiDomainName:
Type: String
Default: api.your-domain.com
Description: API domain name
AuthDomainName:
Type: String
Default: auth.your-domain.com
Description: Auth domain name
SslCertificateArn:
Type: String
Default: arn:aws:acm:us-east-1:123456789:certificate/xxxxx-xxxxx-xxxxx-xxxxx
Description: The SSL certificate ARN
GenerateClientSecret:
Type: String
Default: true
Description: Whether or not to generate client secret
Resources:
ThisVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidrBlock
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Sub ${AppName}-vpc
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ThisVpc
CidrBlock: !Select [0, !Cidr [!Ref VpcCidrBlock, 4, 8]]
MapPublicIpOnLaunch: true
AvailabilityZone: !Select [0, !GetAZs ""]
Tags:
- Key: Name
Value: !Sub ${AppName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ThisVpc
CidrBlock: !Select [1, !Cidr [!Ref VpcCidrBlock, 4, 8]]
MapPublicIpOnLaunch: true
AvailabilityZone: !Select [1, !GetAZs ""]
Tags:
- Key: Name
Value: !Sub ${AppName}-public-subnet-2
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ThisVpc
CidrBlock: !Select [2, !Cidr [!Ref VpcCidrBlock, 4, 8]]
MapPublicIpOnLaunch: false
AvailabilityZone: !Select [0, !GetAZs ""]
Tags:
- Key: Name
Value: !Sub ${AppName}-private-subnet-1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref ThisVpc
CidrBlock: !Select [3, !Cidr [!Ref VpcCidrBlock, 4, 8]]
MapPublicIpOnLaunch: false
AvailabilityZone: !Select [1, !GetAZs ""]
Tags:
- Key: Name
Value: !Sub ${AppName}-private-subnet-2
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AppName}-igw
VpcGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref ThisVpc
InternetGatewayId: !Ref InternetGateway
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ThisVpc
Tags:
- Key: Name
Value: !Sub ${AppName}-public-rt
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ThisVpc
Tags:
- Key: Name
Value: !Sub ${AppName}-private-rt-1
PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref ThisVpc
Tags:
- Key: Name
Value: !Sub ${AppName}-private-rt-2
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable1
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable2
NatEip1:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub ${AppName}-nat-eip-1
NatEip2:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub ${AppName}-nat-eip-2
NatGateway1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip1.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub ${AppName}-nat-1
NatGateway2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip2.AllocationId
SubnetId: !Ref PublicSubnet2
Tags:
- Key: Name
Value: !Sub ${AppName}-nat-2
PrivateRoute1:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway1
PrivateRoute2:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable2
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway2
LambdaRole:
Type: AWS::IAM::Role
DependsOn:
- PrivateRoute1
- PrivateRoute2
Properties:
RoleName: !Sub ${AppName}-lambda-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow inbound traffic on port 443
GroupName: !Sub ${AppName}-lambda-security-group
VpcId: !Ref ThisVpc
LambdaFunction:
Type: AWS::Lambda::Function
DependsOn:
- LambdaRole
- LambdaSecurityGroup
Properties:
FunctionName: !Sub ${AppName}-function
Description: !Sub ${AppName} Lambda function
Code:
ZipFile: |
exports.handler = async (event) => {
console.log(event.requestContext.authorizer.claims);
const sub = event.requestContext.authorizer.claims.sub;
const username = event.requestContext.authorizer.claims.username;
const response = {
statusCode: 200,
body: JSON.stringify(`Hello, World! I am ${username}`),
};
return response;
};
Handler: index.handler
Role: !GetAtt LambdaRole.Arn
Runtime: nodejs18.x
VpcConfig:
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroupIds:
- !GetAtt LambdaSecurityGroup.GroupId
HttpApi:
Type: AWS::ApiGatewayV2::Api
DependsOn: LambdaFunction
Properties:
Name: !Sub ${AppName}-http-api
Description: !Sub ${AppName} HTTP API
ProtocolType: HTTP
DisableExecuteApiEndpoint: false
# https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html
# If you configure CORS for an API, API Gateway automatically sends a response to preflight OPTIONS requests,
# even if there isn't an OPTIONS route configured for your API.
# For a CORS request, API Gateway adds the configured CORS headers to the response from an integration.
CorsConfiguration:
AllowCredentials: false
AllowHeaders:
- Content-Type
- X-Amz-Date,
- X-Amz-Security-Token
- Authorization
- X-Api-Key
- X-Requested-With
- Accept
- Access-Control-Allow-Methods
- Access-Control-Allow-Origin
- Access-Control-Allow-Headers
AllowMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
- PATCH
- HEAD
AllowOrigins:
- "*"
MaxAge: 86400
LambdaPermission:
Type: AWS::Lambda::Permission
DependsOn: HttpApi
Properties:
FunctionName: !GetAtt LambdaFunction.Arn
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*"
ApiIntegration:
Type: AWS::ApiGatewayV2::Integration
DependsOn: HttpApi
Properties:
ApiId: !Ref HttpApi
Description: !Sub ${AppName} API Gateway integration
IntegrationType: AWS_PROXY
IntegrationMethod: ANY
IntegrationUri: !GetAtt LambdaFunction.Arn
PayloadFormatVersion: 1.0
ApiRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref HttpApi
#RouteKey: $default
RouteKey: GET /
AuthorizationType: JWT
AuthorizerId: !Ref JwtAuthorizer
Target: !Join
- /
- - integrations
- !Ref ApiIntegration
# https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html
# Read section: Configuring CORS for an HTTP API with a $default route and JWT authorizer
ApiOptionsRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref HttpApi
RouteKey: "OPTIONS /{proxy+}"
Target: !Join
- /
- - integrations
- !Ref ApiIntegration
HttpApiDomainName:
Type: AWS::ApiGatewayV2::DomainName
Properties:
DomainName: !Ref ApiDomainName
DomainNameConfigurations:
- EndpointType: REGIONAL
CertificateArn: !Ref SslCertificateArn
SecurityPolicy: TLS_1_2
ApiStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
StageName: $default
AutoDeploy: true
ApiId: !Ref HttpApi
DefaultRouteSettings:
DetailedMetricsEnabled: true
ThrottlingBurstLimit: 500
ThrottlingRateLimit: 1000
ApiMapping:
Type: AWS::ApiGatewayV2::ApiMapping
Properties:
ApiId: !Ref HttpApi
DomainName: !Ref ApiDomainName
Stage: !Ref ApiStage
UserPool:
Type: AWS::Cognito::UserPool
DependsOn: ApiIntegration
Properties:
UserPoolName: !Sub ${AppName}-api-user-pool
DeletionProtection: ACTIVE
AdminCreateUserConfig:
AllowAdminCreateUserOnly: False
#AutoVerifiedAttributes:
# This allows user to verify themself with email code, sometimes might not be desired
# Disable(comment out) this to only allow admin to confirm the user and user will get
# "An error was encountered with the requested page." upon signup
# Before user is confirmed by admin, user will also get "User is not confirmed." at login
#- email
Schema:
- Name: name
AttributeDataType: String
Mutable: true
Required: true
- Name: email
AttributeDataType: String
Mutable: false
Required: true
# An "A" record (points to an IP) is required for the root domain for this to work:
# https://repost.aws/knowledge-center/cognito-custom-domain-errors
# If the parent domain doesn't point to a real IP address,
# then consider putting a dummy IP address, such as "8.8.8.8", in your DNS configuration.
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
DependsOn: UserPool
Properties:
UserPoolId: !Ref UserPool
Domain: !Ref AuthDomainName
CustomDomainConfig:
CertificateArn: !Ref SslCertificateArn
AppClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: UserPool
Properties:
AllowedOAuthFlows:
# Authorization code grant - Amazon Cognito issues a code to the client.
# The client can redeem this token at your domain's token endpoint
# for access, ID, and refresh tokens. Only authorization code grants can return refresh tokens.
- code
# Implicit grant - Amazon Cognito issues access and ID tokens directly
# to the client. Implicit grants expose tokens directly to the user.
# You can't issue refresh tokens for this grant type.
- implicit
# Client credential grant - Amazon Cognito issues an access token directly
# to the client for machine-to-machine token exchange. You must use a client secret,
# and have a custom scope configured, to use this grant type.
# You can't use an implict or authorization code grant type at the
# same time as a client credential grant.
#- client_credentials
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
- ALLOW_USER_PASSWORD_AUTH
AllowedOAuthScopes:
#- aws.cognito.signin.user.admin
- openid
- email
- profile
AllowedOAuthFlowsUserPoolClient: true
ClientName: !Sub ${AppName}-api-app-client
UserPoolId: !Ref UserPool
AccessTokenValidity: 12
IdTokenValidity: 12
RefreshTokenValidity: 7
TokenValidityUnits:
AccessToken: hours
IdToken: hours
RefreshToken: days
GenerateSecret: !Ref GenerateClientSecret
SupportedIdentityProviders:
- COGNITO
CallbackURLs:
- !Sub https://${AuthDomainName}
LogoutURLs:
- !Sub https://${AuthDomainName}
JwtAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref HttpApi
AuthorizerType: JWT
IdentitySource:
- $request.header.Authorization
JwtConfiguration:
Audience:
- !Ref AppClient
Issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}
Name: !Sub ${AppName}-jwt-authorizer
ApiRecordSet:
Type: AWS::Route53::RecordSet
DependsOn: HttpApiDomainName
Properties:
# HostedZoneName wants a "." at the end
HostedZoneName: !Join
- ""
- - !Join
- "."
- - !Select [1, !Split [".", !Ref ApiDomainName]]
- !Select [2, !Split [".", !Ref ApiDomainName]]
- "."
Name: !Ref ApiDomainName
Type: A
AliasTarget:
# https://docs.aws.amazon.com/general/latest/gr/apigateway.html
HostedZoneId: Z1UJRXOUMOOFQ8
DNSName: !GetAtt HttpApiDomainName.RegionalDomainName
EvaluateTargetHealth: false
CognitoUiRecordSet:
Type: AWS::Route53::RecordSet
DependsOn: UserPoolDomain
Properties:
# HostedZoneName wants a "." at the end
HostedZoneName: !Join
- ""
- - !Join
- "."
- - !Select [1, !Split [".", !Ref AuthDomainName]]
- !Select [2, !Split [".", !Ref AuthDomainName]]
- "."
Name: !Ref AuthDomainName
Type: A
AliasTarget:
# https://docs.aws.amazon.com/general/latest/gr/cf_region.html
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt UserPoolDomain.CloudFrontDistribution
EvaluateTargetHealth: false
Outputs:
UserPoolId:
Value: !Ref UserPool
Description: Cognito user pool ID
# This value changes if you update the UserPoolClient
AppClientId:
Value: !Ref AppClient
Description: Cognito App client ID
SignupGetTokenURL:
# Implicit grant
# redirect_uri must be in the CallbackURLs defined in the UserPoolClient
Value: !Sub https://${AuthDomainName}/login?client_id=${AppClient}&response_type=token&scope=email+profile&redirect_uri=https://${AuthDomainName}
Description: Cognito signup/login URL
SignupGetCodeURL:
# Authorization code grant
# redirect_uri must be in the CallbackURLs defined in the UserPoolClient
Value: !Sub https://${AuthDomainName}/login?client_id=${AppClient}&response_type=code&scope=email+profile&redirect_uri=https://${AuthDomainName}
Description: Cognito signup/login URL
LogoutURL:
# logout_uri must be in the LogoutURLs defined in the UserPoolClient
Value: !Sub https://${AuthDomainName}/logout?client_id=${AppClient}&logout_uri=https://${AuthDomainName}
Description: Cognito logout URL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment