I’ve been back at the Cloudformation in the last little while as we’ve been provisioning some new clients at work and I wanted to speed things up substantially. This led me down a bit of a rabbit hole experimenting with various parts that we’ve previously done using ad-hoc clickops, including Cognito user pools. I found there wasn’t really any complete examples out there for me to rip off, so I’ll dump what I came up with here.
The Template
Let’s start off with what everyone wants to see, the Cloudformation template
AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template
Parameters:
UserEmail:
Type: String
Description: Test user's email
AllowedCallbacks:
Type: List<String>
Description: List of URLs that the application is allowed to redirect to
AuthDomainParam:
Type: String
Description: Cognito auth domain
Resources:
CognitoUsers:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: test-pool
UsernameConfiguration:
CaseSensitive: false
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireSymbols: true
RequireUppercase: true
TemporaryPasswordValidityDays: 1
UsernameAttributes:
- email
MfaConfiguration: "OFF"
Schema:
- AttributeDataType: String
DeveloperOnlyAttribute: false
Mutable: true
Name: email
ServerAppClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref CognitoUsers
ClientName: ServerClient
GenerateSecret: true
RefreshTokenValidity: 30
AllowedOAuthFlows:
- code
- implicit
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
CallbackURLs: !Ref AllowedCallbacks
AllowedOAuthScopes:
- email
- openid
- profile
AllowedOAuthFlowsUserPoolClient: true
PreventUserExistenceErrors: ENABLED
SupportedIdentityProviders:
- COGNITO
ClientAppClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref CognitoUsers
ClientName: ClientApp
GenerateSecret: false
RefreshTokenValidity: 30
AllowedOAuthFlows:
- code
- implicit
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
CallbackURLs: !Ref AllowedCallbacks
AllowedOAuthScopes:
- email
- openid
- profile
- aws.cognito.signin.user.admin
AllowedOAuthFlowsUserPoolClient: true
PreventUserExistenceErrors: ENABLED
SupportedIdentityProviders:
- COGNITO
AuthDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
UserPoolId: !Ref CognitoUsers
Domain: !Ref AuthDomainParam
TestUser:
Type: AWS::Cognito::UserPoolUser
Properties:
UserPoolId: !Ref CognitoUsers
Username: !Ref UserEmail
UserAttributes:
- Name: email
Value: !Ref UserEmail
TestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: Test Cognito Auth
Description: Testing the user pool
TestResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref TestApi
PathPart: test
ParentId:
Fn::GetAtt:
- TestApi
- RootResourceId
TestAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
IdentitySource: method.request.header.authorization
Name: CognitoAuthorizer
ProviderARNs:
- Fn::GetAtt:
- CognitoUsers
- Arn
RestApiId: !Ref TestApi
Type: COGNITO_USER_POOLS
ApiGatewayModel:
Type: AWS::ApiGateway::Model
Properties:
ContentType: 'application/json'
RestApiId: !Ref TestApi
Schema: {}
TestMethod:
Type: AWS::ApiGateway::Method
Properties:
ApiKeyRequired: false
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref TestAuthorizer
HttpMethod: GET
Integration:
IntegrationHttpMethod: GET
RequestTemplates:
application/json: '{"statusCode": 200}'
IntegrationResponses:
- ResponseTemplates:
application/json: "{\"message\": \"Hello from API gateway\"}"
SelectionPattern: '2\d{2}'
StatusCode: 200
- ResponseTemplates:
application/json: "{\"message\": \"Endless fucking trash\"}"
SelectionPattern: '5\d{2}'
StatusCode: 500
PassthroughBehavior: WHEN_NO_TEMPLATES
Type: MOCK
TimeoutInMillis: 29000
MethodResponses:
- ResponseModels:
application/json: !Ref ApiGatewayModel
StatusCode: 200
- ResponseModels:
application/json: !Ref ApiGatewayModel
StatusCode: 500
OperationName: 'mock'
ResourceId: !Ref TestResource
RestApiId: !Ref TestApi
OptionsMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
RestApiId:
Ref: TestApi
ResourceId:
Ref: TestResource
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: WHEN_NO_MATCH
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
- StatusCode: 200
ResponseModels:
application/json: 'Empty'
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: false
method.response.header.Access-Control-Allow-Methods: false
method.response.header.Access-Control-Allow-Origin: false
# Need a way to force this to update, still looking for something easy
TestDeploy:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref TestApi
StageName: test
Outputs:
UserPoolId:
Description: The user pool ID
Value: !Ref CognitoUsers
UserPoolUrl:
Description: URL of the Cognito provider
Value:
Fn::GetAtt:
- CognitoUsers
- ProviderURL
ClientId:
Description: The app client ID
Value: !Ref ClientAppClient
The Breakdown
In order to use Cognito in an OAuth application, we need three things:
- A user pool, where we can create and authorize users, set scopes, etc
- An application client that uses the user pool, and can handle the OAuth flow
- An authentication domain where our users can login
This template also sets up our API Gateway endpoint, which has a mock integration to check to make sure everything is working correctly, and an authorizer to do our token checks for us.