Instantly share code, notes, and snippets.
Created
July 10, 2023 02:57
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save alanzhaonys/b30ec8057817ae1ee2129684bd9a3d6c to your computer and use it in GitHub Desktop.
Secure API Gateway with Cognito
This file contains 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
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 | |
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 | |
- 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