AWSCDK

Create AWS Cognito UserPool using CDK

user image
Anoop Nair@sudodeznit
October 6th, 20229 min read

We will be creating a new Amazon Cognito Userpool and app client. We will look into different option available to us for configuring cognito authentication.

cover image

Introduction

Amazon cognito userpool is a secure identity store that lets you add user sign-up, sign-in, and access control to aws resources from your web and mobile apps quickly and easily. An App client is an entity within userpool that lets you call unauthenticated api like signin, signup from your app, App client have an app id and optional secure secret so that only authorised client app can call these unauthorised apis.

Getting started

We will be writing cdk code in typescript and will use CDK toolkit aws-cdk to initialise a starter project. We will first create a new folder cdk-cognito-userpool and cd into the folder, the name of the folder will be used by cdk toolkit to name the stack but it can be changed later.

mkdir cdk-cognito-userpool
cd cdk-cognito-userpool

We can now run the following command to initialise project will be using the default app template.

npx aws-cdk init app --language=typescript

Only dependency that we have to add is aws-cdk-lib

npm i aws-cdk-lib

Defining Stack

We will define the stack inside lib > cdk-cognito-userpool-stack.ts . Lets start by creating a userpool basic and we will add configuration option later on.

Userpool

We will use the Userpool construct from cognito to create userpool.

  • userPoolName property can be used to define a custom name for cogninto userpool resource. If this property is not defined CloudFormation will automatically generate a name for the resource.
  • selfSignUpEnabled if this is set to false then a user can only be signed up by admin.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as cognito from "aws-cdk-lib/aws-cognito";

export class CdkCognitoUserpoolStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
      /**
       * you can specify userPoolName if not cloudformation will generate a name
       */
      userPoolName: "deznit-userpool",
      /**
       * if false user can only be invided by admin
       */
      selfSignUpEnabled: true,
    });
  }
}

Customising emails

  • userVerificationis used to customise the email sent to user on signup.
  • userInvitation is email sent to user when a new user is created by admin.
  • emailStyle have two options cognito.VerificationEmailStyle.CODE which sends an otp, and cognito.VerificationEmailStyle.LINK which will send a verification link in the email.
  • You can use HTML tags inside emailBody for styling. if you use CODE as verification type the emailBody must contain {###} as a placeholder to insert OTP by cognito. If you use LINK then emailBody must contain {##Verify Your Email##} as placeholder for the link.
const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
  // ... rest

  /**
   * customizing verification email
   */
  userVerification: {
    emailStyle: cognito.VerificationEmailStyle.CODE,
    emailSubject: "Deznit email verification",
    emailBody:
      "Thanks for signing up to Deznit Your verification code is {####}",
  },

  /**
   * customising admin invite
   */
  userInvitation: {
    emailSubject: "Invitation to join Deznit",
    emailBody:
      "Hello {username}, you have been invited to join Deznit. Your temporary password is {####}",
  },
});

The maximum length for the message, including the verification code (if present), is 20,000 UTF-8 characters

Sign In

There are 4 different methods (username, email , phone , prefferedUsername ) to let users signin to your app. This cannot be changed once userpool is created .

  • username is immutable, user will have to set an username at the time of signup.
  • prefferedUsername this is mutable and only available if username option is set to true.

If email address is selected as an alias, Amazon Cognito doesn't accept a user name that matches a valid email address format. Similarly, if you select phone number as an alias, Amazon Cognito doesn't accept a user name for that user pool that matches a valid phone number format.

  • Aliases can only be used for signin if its verified so it’s important to set autoVerify to true for the aliases.
  • If you want to ignore case checking for aliases like username you can set signInCaseSensitive to false, default value is true.
  • Password policy can be configured using the passwordPolicy property. temporaryPasswordValidityDays is the validity to password generated when a user is invited to app, if user doesn’t signup in the timeframe admin will have to reset the password.
  • accountRecovery is how user will be able to recover their account if they forget their password. Default value is phone if available otherwise email. User wont be able to use phone for recovery if it is used for MFA.
const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
  // ...rest
  signInAliases: { email: true },
  /**
   * cognito will request verification for following
   */
  autoVerify: { email: true },
  signInCaseSensitive: false, //default true

  passwordPolicy: {
    minLength: 6,
    requireDigits: true,
    requireLowercase: false,
    requireUppercase: false,
    requireSymbols: false,
    temporaryPasswordValidityDays: 30,
  },
  /**
   * This is how user will be able to recover their account
   */
  accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
});

Attributes

We can associate attributes with a cognito user. Cognito have some predefined attributes called standardAttributes which when set as required user wont be able to signup without providing that info. Apart from the standard attributes we can also set am maximum of 50 customAttributes also.

const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
  // ...rest

  /**
   * Standard attributes that cognito supports
   * if required user wont be able to signup unless that attribute is provided
   */
  standardAttributes: {
    fullname: {
      required: false,
      mutable: true,
    },
    gender: {
      required: false,
      mutable: true,
    },
  },
  /**
   * custom attributes for a user
   * max 50
   */
  customAttributes: {
    isAdmin: new cognito.BooleanAttribute({ mutable: true }),
    level: new cognito.StringAttribute({ mutable: true }),
  },
});

With this we have setup a basic cognito userPool, there are many other features like setting MFA, SES for sms and lambda triggers etc. which I will cover in future posts. Now below the userpool let’s create a Userpool App Client.

Removal policy

removalPolicy specify if we want to DESTROY or RETAIN the userPool on running cdk destroy. Default value is RETAIN.

const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
  // ...rest

  /**
   * Deletes the userpool on cdk destroy
   * default is RETAIN
   */
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

App Client

An App client lets you call unauthenticated api like signin, signup from your app, App client have an app id and optional secure secret so that only authorised client app can call these unauthorised apis.

  • cognito.ClientAttributes() can be used to grant read and write permissions to cognito attributes from client app.
  • Read and write permissions can be provided to both standardAttributes and customAttributes
  • Provide permissions to only those attributes which you need. For example you should not give write permissions to attributes like isAdmin
const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
  // ...userpool configuration
});

/**
 * userpool client permissions to write read attributes
 */

const clientWriteAttributes = new cognito.ClientAttributes()
  .withStandardAttributes({ fullname: true, gender: true })
  .withCustomAttributes("isAdmin", "level");

const clientReadAttributes = clientWriteAttributes
  .withStandardAttributes({
    fullname: true,
    gender: true,
    emailVerified: true,
    preferredUsername: true,
  })
  .withCustomAttributes("isAdmin", "level");

/**
 * Adding userpool client
 */

const userpoolClient = deznitUserpool.addClient("app-client", {
  userPoolClientName: "deznit-app-client",

  readAttributes: clientReadAttributes,
  writeAttributes: clientWriteAttributes,
});

Final Code

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as cognito from "aws-cdk-lib/aws-cognito";

export class CdkCognitoUserpoolStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const deznitUserpool = new cognito.UserPool(this, "deznituserpool", {
      /**
       * you can specify userPoolName if not cloudformation will generate a name
       */
      userPoolName: "deznit-userpool",
      /**
       * if false user can only be invided by admin
       */
      selfSignUpEnabled: true,

      /**
       * customizing verification email
       */
      userVerification: {
        emailStyle: cognito.VerificationEmailStyle.CODE,
        emailSubject: "Deznit email verification",
        emailBody:
          "Thanks for signing up to Deznit Your verification code is {####}",
      },
      /**
       * customising admin invite
       */
      userInvitation: {
        emailSubject: "Invitation to join Deznit",
        emailBody:
          "Hello {username}, you have been invited to join Deznit. Your temporary password is {####}",
      },
      /**
       * ways by which users can signin
       * 4 options --> username, email, phone, preferredUsername
       *
       */
      signInAliases: { email: true },
      /**
       * cognito will request verification for following
       */
      autoVerify: { email: true },
      signInCaseSensitive: false,

      passwordPolicy: {
        minLength: 6,
        requireDigits: true,
        requireLowercase: false,
        requireUppercase: false,
        requireSymbols: false,
      },

      /**
       * Standard attributes that cognito supports
       * if required user wont be able to signup unless that attribute is provided
       */
      standardAttributes: {
        fullname: {
          required: false,
          mutable: true,
        },
        gender: {
          required: false,
          mutable: true,
        },
      },
      /**
       * custom attributes for a user
       * max 50
       */
      customAttributes: {
        isAdmin: new cognito.BooleanAttribute({ mutable: true }),
        level: new cognito.StringAttribute({ mutable: true }),
      },

      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,

      /**
       * The email from which cognito sends email
       * for higher volume use Amazon SES
       */
      email: cognito.UserPoolEmail.withCognito("info@deznit.com"),

      /**
       * Deletes the userpool on cdk destroy
       * default is RETAIN
       */
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    /**
     * userpool client permissions to write read attributes
     */

    const clientWriteAttributes = new cognito.ClientAttributes()
      .withStandardAttributes({ fullname: true, gender: true })
      .withCustomAttributes("isAdmin", "level");

    const clientReadAttributes = clientWriteAttributes
      .withStandardAttributes({
        fullname: true,
        gender: true,
        emailVerified: true,
        preferredUsername: true,
      })
      .withCustomAttributes("isAdmin", "level");

    /**
     * Adding userpool client
     */

    const userpoolClient = deznitUserpool.addClient("app-client", {
      userPoolClientName: "deznit-app-client",

      readAttributes: clientReadAttributes,
      writeAttributes: clientWriteAttributes,
    });

    /**
     * print values to console
     */
    new cdk.CfnOutput(this, "aws_user_pools_id", {
      value: deznitUserpool.userPoolId,
    });

    new cdk.CfnOutput(this, "aws_user_pools_web_client_id", {
      value: userpoolClient.userPoolClientId,
    });
  }
}
  • To synthesize and print cloudformation template run
cdk synth
  • To deploy the stack to aws run
cdk deploy
  • To test the userpool lets create a new user using the aws cli create-admin-user. You can find YOUR_USERPOOL_ID from cdk-exports.ts file create after cdk deploy.
aws cognito-idp admin-create-user \
  --user-pool-id YOUR_USER_POOL_ID \
  --username deznit.text@gmail.com

You will get response similar to this after successful creation of user in cognito and in aws console you will be able to see the newly created user, an email will be sent with temporary password to the user.

{
  "User": {
    "Username": "59be41de-2c0e-4e4f-adf4-945b5fd2a56e",
    "Attributes": [
      {
        "Name": "sub",
        "Value": "59be41de-2c0e-4e4f-adf4-945b5fd2a56e"
      },
      {
        "Name": "email",
        "Value": "deznit.text@gmail.com"
      }
    ],
    "UserCreateDate": "2022-10-08T11:39:22.261000+05:30",
    "UserLastModifiedDate": "2022-10-08T11:39:22.261000+05:30",
    "Enabled": true,
    "UserStatus": "FORCE_CHANGE_PASSWORD"
  }
}

user image

  • To delete the stack run the following command (if removalPolicy is set to DESTROY it will delete the userpool and all the user data)
cdk destroy