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 ClientAppClientThe 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.