Cloudformation API Gateway with Cognito Authorizer

Filed under aws on August 24, 2020

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.