Ktor with Cognito
Oct 9, 2019
Ktor with Cognito

We recently spiked a new mobile project that required an authenticated API backend. I wanted to use this opportunity to explore a new API framework. My requirements were that it be asynchronous, lightweight, and flexible. I also wanted to work in a modern, statically-typed language. Ktor and Kotlin met these requirements and are a pleasure to use. An added benefit of using Kotlin for the mobile backend is that it doesn’t introduce a third programming language into the mix (assuming Swift and Kotlin are used for the mobile frontend). In these posts I’ll share how easy it is to implement an API in Ktor that integrates with AWS Cognito user pools for JWT authentication. This first installment will cover creating an AWS Cognito UserPool. In the next installment we will use this UserPool to created an authenticated API in Ktor.

AWS Cognito

AWS Cognito is a service that provides all aspects of user registration and authentication. It is very inexpensive and secure (especially compared to implementing similar services yourself). It generates JSON Web Tokens (JWT) that contain encrypted claims about the user. These tokens are first obtained by authenticating with a Cognito User Pool and can then be included in the Authorization header of subsequent requests.

I’m going to assume you are familiar with AWS CloudFormation and simply provide the stack template to create the AWS resources we will need at the bottom of this page. I typically checkout and execute these templates from a Cloud9 instance using the provided AWS CLI. Assuming you save this template in a file called /home/ec2-user/environment/stack.local.cfn.yaml the command to create this "local-cognito" stack for local development using the AWS CLI is:

aws cloudformation create-stack \
  --capabilities CAPABILITY_NAMED_IAM \
  --stack-name local-cognito \
  --template-body file:///home/ec2-user/environment/stack.local.cfn.yaml 

User Pool:

Assuming your stack is created successfully you should have some new resources in your AWS environment. The first thing the template creates is a User Pool named "local-ApiUserPool". If you navigate to the Cognito UI console, you should see it there. The AllowAdminCreateUserOnly: true property requires that new users be created by an admin user (i.e. no self-registration). Our User Pool enables users to login with their verified email address. Cognito takes care of emailing a temporary password to users for initial login. As you can see in the template, we are able to specify a password policy and then configure which of the standard attributes are required. In our case, user records must contain an email, given_name, and family_name. Custom attributes can be added to store additional user information beyond the standard attributes, but we have no need in our example.

User Pool Client:

Now that we have a User Pool defined, we must create a client configuration that specifies how our API can interact with our User Pool. The ExplicitAuthFlows: - ADMIN_NO_SRP_AUTH option is required to enable our server-side API to authenticate on the user's behalf. We further specify that our API may read/write the three required user attributes and that JWT refresh tokens shall remain valid for the max of 3650 days. In order for our API to access Cognito using the AWS Java SDK, we create an IAM user (local-CognitoAdminUser) with a policy granting it access to our User Pool. Because we will be running our API locally (as opposed to spinning it up in Elastic Beanstalk), we need to manually generate an access and secret key for this IAM user. This can be done by finding this "local-CognitoAdminUser" user in the IAM UI console and creating an access key from the Security Credentials tab. Copy and save the key and secret values as we will need to set them as environment variables for our API.

Create a user:

We can create a new user in the user pool using the aws CLI command below. Replace the ? values with your information. The user-pool-id can be found in the outputs section of your cloud formation stack. You should receive an email with a temporary password.

aws cognito-idp admin-create-user \
  --user-pool-id ? \
  --username ? \
  --user-attributes '[{"Name": "given_name", "Value": "?"}, {"Name": "family_name", "Value": "?"}]'

If you get this email, congratulations, you now have a fully functional Cognito UserPool to use with your API! In the next post, we will cover how to integrate with this UserPool to create an authenticate API in Ktor.

AWS CloudFormation Template

AWSTemplateFormatVersion: "2010-09-09"
Description: "Creates and configures resources for local dev env."

Resources:
  ApiUserPool:
    Type: AWS::Cognito::UserPool
    Properties: 
      UserPoolName: "local-ApiUserPool"
      AdminCreateUserConfig: 
        AllowAdminCreateUserOnly: true
      AutoVerifiedAttributes:
        - email
      UsernameAttributes:
        - email
      EmailConfiguration: 
        EmailSendingAccount: COGNITO_DEFAULT
      Policies: 
        PasswordPolicy:
          MinimumLength: "8"
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
          TemporaryPasswordValidityDays: "14"
      Schema: 
        -
          AttributeDataType: String
          Mutable: false
          Name: email
          Required: true
        -
          AttributeDataType: String
          Mutable: true
          Name: given_name
          Required: true
        -
          AttributeDataType: String
          Mutable: true
          Name: family_name
          Required: true
          
  ApiUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties: 
      ClientName: "local-ApiUserPoolClient"
      ExplicitAuthFlows: 
        - ADMIN_NO_SRP_AUTH
      GenerateSecret: false
      ReadAttributes: 
        - email
        - given_name
        - family_name
      RefreshTokenValidity: "3650"
      UserPoolId: !Ref ApiUserPool
      WriteAttributes: 
        - email
        - given_name
        - family_name

  CognitoAdminUser:
    Type: AWS::IAM::User
    Properties: 
      UserName: "local-CognitoAdminUser"
      Policies: 
        - 
          PolicyName: "local-CognitoAdminUserPolicy"
          PolicyDocument: 
            Version: '2012-10-17'
            Statement:
              - 
                Effect: Allow
                Action:
                  - "cognito-idp:*"
                Resource: !GetAtt ApiUserPool.Arn
                
Outputs:
  UserPoolId:
    Description: "Id of api user pool"
    Value: !Ref ApiUserPool
    Export: 
      Name: "local-UserPoolId"
      
  UserPoolClientId:
    Description: "Id of api user pool client"
    Value: !Ref ApiUserPoolClient
    Export: 
      Name: "local-UserPoolClientId"
      
  UserPoolEndpoint:
    Description: "UserPool Endpoint"
    Value: !GetAtt ApiUserPool.ProviderURL
    Export: 
      Name: "local-UserPoolEndpoint"

Stuart Williamson, Phase 2 Senior Software Engineer
Stuart Williamson Author
Stuart Williamson is a Software Engineer who is continually blown away by the pace of power of cloud technologies.