Cognito User Pool Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Last updated: Apr 14, 2022

banner

Check out my new book

Cognito User Pool & User Pool Client Example in CDK #

In this article we're going to use CDK to provision a Cognito User Pool and a User Pool Client. We'll go through a step-by-step explanation of the different configuration options and things we should be aware of.

In order to provision a Cognito User Pool in CDK we have to use the UserPool construct.

A User pool is the directory where we store and manage users in AWS Cognito. A User Pool allows our users to register and sign in, and allows us to manage their user profiles.

Cognito User Pool in AWS CDK - Example #

The code for this article is available on GitHub

I'll post a snippet of a User Pool with some common configuration properties and we are going to go over the code:

lib/cdk-starter-stack.ts
import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // 👇 User Pool const userPool = new cognito.UserPool(this, 'userpool', { userPoolName: 'my-user-pool', selfSignUpEnabled: true, signInAliases: { email: true, }, autoVerify: { email: true, }, standardAttributes: { givenName: { required: true, mutable: true, }, familyName: { required: true, mutable: true, }, }, customAttributes: { country: new cognito.StringAttribute({mutable: true}), city: new cognito.StringAttribute({mutable: true}), isAdmin: new cognito.StringAttribute({mutable: true}), }, passwordPolicy: { minLength: 6, requireLowercase: true, requireDigits: true, requireUppercase: false, requireSymbols: false, }, accountRecovery: cognito.AccountRecovery.EMAIL_ONLY, removalPolicy: cdk.RemovalPolicy.RETAIN, }); } }

Let's go over the configuration properties we've passed to the User Pool:

  • selfSignUpEnabled - whether users are allowed to sign up
  • signInAliases - whether users should sign in via email, phone number or username
  • autoVerify - specify attributes that Cognito will automatically request verification for, when a user signs up. Allowed values are email or phone.
  • standardAttributes - specify required standard attributes that users must provide when signing up
  • customAttributes - specify a map of non-standard attributes that allow you to store user profile information - i.e. shipping address, billing information, etc.
  • passwordPolicy - set a password policy to force users to sign up with secure passwords
  • accountRecovery - specify how users should recover their account in case they forget their password
  • removalPolicy - specify whether the User Pool should be retained in the account after the stack is deleted. The default behavior is for the User Pool to be retained in the account (in an orphaned state).

Cognito sends emails to users when they register, forget their password, etc.

By default, the emails are sent from no-reply@verificationemail.com. For production you're more than likely going to have to integrate with SES and configure your own email address.

At the time of writing in order to set our own Email configuration, we have to use an escape hatch and update the emailConfiguration property on the Level 1 CfnUserPool construct.

If you have to update the email Cognito uses, when sending emails to users, you can use the following snippet:

lib/cdk-starter-stack.ts
const userPool = new cognito.UserPool(this, 'userpool', { //...rest }) // 👇 OPTIONALLY update Email sender for Cognito Emails const cfnUserPool = userPool.node.defaultChild as cognito.CfnUserPool; cfnUserPool.emailConfiguration = { emailSendingAccount: 'DEVELOPER', replyToEmailAddress: 'YOUR_EMAIL@example.com', sourceArn: `arn:aws:ses:YOUR_COGNITO_SES_REGION:${ cdk.Stack.of(this).account }:identity/YOUR_EMAIL@example.com`, };

Let's go over the configuration properties in the code snippet:

  • emailSendingAccount specifies whether Cognito uses the default emails or our own SES email configuration. DEVELOPER means that we have to provide our own SES configuration.
  • replyToEmailAddress specifies the email that will receive replies from users
  • sourceArn specifies the ARN of a verified SES email address. However, note that Cognito only supports SES in 3 regions - us-east-1, us-west-2 and eu-west-1.
Cognito ONLY supports SES in the us-east-1, us-west-2, eu-west-1 regions.

I'll now run the deployment command (without the SES configuration code):

shell
npx aws-cdk deploy

The CloudFormation console shows that the User Pool resource has been provisioned:

cloudformation console pool

Cognito User Pool Client in AWS CDK - Example #

Next, we're going to add a User Pool client to our Cognito User Pool.

The User Pool Client is the part of the User Pool that enables unauthenticated operations like register, sign in and restore forgotten password. You can't call these operations without an app client ID, which you get by creating a User Pool Client.

I'll post a complete User Pool Client snippet and then we're going to go over the configuration options. The code should be placed right below the User Pool code.

lib/cdk-starter-stack.ts
// 👇 User Pool Client attributes const standardCognitoAttributes = { givenName: true, familyName: true, email: true, emailVerified: true, address: true, birthdate: true, gender: true, locale: true, middleName: true, fullname: true, nickname: true, phoneNumber: true, phoneNumberVerified: true, profilePicture: true, preferredUsername: true, profilePage: true, timezone: true, lastUpdateTime: true, website: true, }; const clientReadAttributes = new cognito.ClientAttributes() .withStandardAttributes(standardCognitoAttributes) .withCustomAttributes(...['country', 'city', 'isAdmin']); const clientWriteAttributes = new cognito.ClientAttributes() .withStandardAttributes({ ...standardCognitoAttributes, emailVerified: false, phoneNumberVerified: false, }) .withCustomAttributes(...['country', 'city']); // 👇 User Pool Client const userPoolClient = new cognito.UserPoolClient(this, 'userpool-client', { userPool, authFlows: { adminUserPassword: true, custom: true, userSrp: true, }, supportedIdentityProviders: [ cognito.UserPoolClientIdentityProvider.COGNITO, ], readAttributes: clientReadAttributes, writeAttributes: clientWriteAttributes, });

Let's go over the code snippet:

  • the clientReadAttributes variable represents the standard and custom attributes our application is going to be able to read on cognito users. For a reference I've included all of the standard attributes that cognito supports and 3 custom attributes - country, city and isAdmin.

  • the clientWriteAttributes variable represents the attributes the User Pool Client will be able to write. We've removed the emailVerified and phoneNumberVerified properties from the write attributes, as users shouldn't be able to verify their own email or phone number. Note that the custom attributes don't include isAdmin, since we don't want users to be able to set their isAdmin property.

A common Cognito error is - "Invalid write attributes specified while creating a client". The cause might be that we've mistyped an attribute or we've tried to include properties that a user should not be able to write, i.e. emailVerified or phoneNumberVerified

error write attributes

Moving on to the User Pool Client, the configuration properties are:

  • userPool - the User Pool this client will be able to access
  • authFlows - the types of authentication flows enabled for the client.
  • supportedIdentityProviders - the identity providers the client supports. This is where we'd add Facebook IDP and Google IDP if we were to use OAuth.
  • readAttributes - the attributes the client is able to read
  • writeAttributes - the attributes the client is able to write

I'll now run a deployment to provision the User Pool Client:

shell
npx aws-cdk deploy

The CloudFormation console shows that our User Pool Client has been provisioned as well:

cloudformation console client

Next, I'll add Output values for the userPoolId and userPoolClientId, so we can test our Cognito resources using the AWS CLI:

// 👇 Outputs new cdk.CfnOutput(this, 'userPoolId', { value: userPool.userPoolId, }); new cdk.CfnOutput(this, 'userPoolClientId', { value: userPoolClient.userPoolClientId, });

I'll write the Outputs to a file by providing the --outputs-file to the deploy command:

shell
npx aws-cdk deploy --outputs-file ./cdk-outputs.json

After I've ran the command, a cdk-outputs.json file has been generated in the root directory of my project:

cdk-outputs.json
{ "cdk-starter-stack": { "userPoolClientId": "7bmqsuj8gcth6qphick1hug4pn", "userPoolId": "eu-central-1_aXqszS5Mq" } }

These are the identifiers I'll use in order to test the provisioned Cognito resources.

Testing our Cognito User Pool and User Pool Client #

If you run these commands make sure to update the YOUR_USER_POOL_ID placeholder with the value from cdk-outputs.json
  1. Creating a Cognito User:
shell
aws cognito-idp admin-create-user \ --user-pool-id YOUR_USER_POOL_ID \ --username john@example.com \ --user-attributes Name="given_name",Value="john" \ Name="family_name",Value="smith"

After running the command, the user is created successfully:

cognito console user

The given and family name standard attributes that we set to required when creating the User Pool have also been updated:

cognito names updated

  1. Updating Standard User attributes:
shell
aws cognito-idp admin-update-user-attributes \ --user-pool-id YOUR_USER_POOL_ID \ --username john@example.com \ --user-attributes Name="gender",Value="m" Name="name",Value="john smith"
  1. Updating Custom User attributes:
shell
aws cognito-idp admin-update-user-attributes \ --user-pool-id YOUR_USER_POOL_ID \ --username john@example.com \ --user-attributes Name="custom:country",Value="Chile" \ Name="custom:city",Value="Santiago" \ Name="custom:isAdmin",Value="yes"

If I refresh the Cognito console and open the User's profile I can see that all of the attributes have been updated:

updated user attributes

At this point we have working User Pool and User Pool Client, provisioned by AWS CDK.

Clean up #

To delete the resources we've provisioned we have to issue the destroy command and manually delete the User Pool because its retention policy is defaulted to RETAIN.

shell
npx aws-cdk destroy

Further Reading #

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.