Cognito User Pool Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Mon Apr 19 20216 min read

banner

Photo by Andrew Ly

In this article we provision a Cognito User Pool and a User Pool Client in AWS CDK, using the UserPool and UserPoolClient constructs.

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.

The User pool is the directory where we store and manage users in AWS Cognito. The 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/aws-cognito';
import * as cdk from '@aws-cdk/core';

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 execute a deployment (without the SES configuration code):

shell
npx 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 / 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 execute a deployment to provision the User Pool Client:

shell
npx 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 cdk deploy \
  --outputs-file ./cdk-outputs.json

After I've executed 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 execute the destroy command and manually delete the User Pool, because its retention policy is defaulted to RETAIN.

shell
npx cdk destroy

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee