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"