DEV Community

Cover image for Solving User Consistency Issues with AWS Cognito
Nestor Gutiérrez
Nestor Gutiérrez

Posted on

Solving User Consistency Issues with AWS Cognito

A few days ago, I was developing an application to store my records (PRs) while practicing sports such as CrossFit and Weightlifting. Everything seemed to be working correctly in what I called my production environment. However, at some point, a significant issue arose: someone managed to register with the same email address and create two almost identical users username !== Username.

This was a major inconvenience, as the person could log in with both users and would not have their information consistently.

The Problem

After a bit of research (I assumed AWS Cognito prevented this behavior automatically 😔), I discovered that a possible solution was to set the email as a sign-in alias. This required making some changes to the Cognito User Pool.

However, there was a problem: I already had registered users, and Cognito does not allow modifying sign-in aliases in an existing User Pool.

The Solution

Create a New User Pool

The solution was to create a new User Pool. Thus, my User Pool V2 was born (yes, a very original name). Once created, I tested and confirmed that:

  1. It was not possible to register a user with the same email.
  2. The system had to be case-insensitive: a user must always be unique, regardless of uppercase or lowercase.

To handle the latter, I discovered Pre-Signup Triggers. These triggers allow you to run a Lambda Function before completing the signup process. In this function, I implemented the following logic:


import boto3

client = boto3.client('cognito-idp')

def lambda_handler(event, context):
    user_pool_id = event['userPoolId']
    username = event['userName'].lower()  # Normalizes the username
    email = event['request']['userAttributes']['email'].lower()  # Normalizes the email

    # Validate username uniqueness
    response = client.list_users(
        UserPoolId=user_pool_id,
        Filter=f'username = "{username}"'
    )
    if len(response['Users']) > 0:
        raise Exception("The username is already registered.")

    # Validate email uniqueness
    response = client.list_users(
        UserPoolId=user_pool_id,
        Filter=f'email = "{email}"'
    )
    if len(response['Users']) > 0:
        raise Exception("The email is already registered.")

    # Normalize username and email in the event
    event['userName'] = username
    event['request']['userAttributes']['email'] = email

    return event
Enter fullscreen mode Exit fullscreen mode
  1. Convert data to lowercase (username and email).
  2. Check if the user already exists.
  3. Update the event and return it.

With this, we ensure data consistency and prevent duplicate users, regardless of uppercase or lowercase usage.

Deployment with AWS CDK

To deploy this solution, I used AWS CDK with TypeScript. An example configuration might look like this:

import { Construct } from "constructs";
import { addTags } from "../common/utilities/addTag";
import { RetentionDays } from "aws-cdk-lib/aws-logs";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { PolicyStatement,Effect } from "aws-cdk-lib/aws-iam";
import { CfnOutput,NestedStack,NestedStackProps} from "aws-cdk-lib";
import { PROJECT_NAME,ENVIRONMENT, COGNITO_CALLBACK_URL } from "../../constants";
import { Code, Function, ILayerVersion, LayerVersion, Runtime } from "aws-cdk-lib/aws-lambda";
import { UserPool,AccountRecovery,VerificationEmailStyle,ClientAttributes,UserPoolClient,OAuthScope } from "aws-cdk-lib/aws-cognito";

export class UserPoolStack extends NestedStack {
  private projectEnv: string;

  public readonly userPoolV2: UserPool;
  public readonly userPoolClientV2: UserPoolClient;

  constructor(scope: Construct,id: string,props?: NestedStackProps) {
    super(scope,id,props);

    const REGION = this.region;

    this.projectEnv = `${PROJECT_NAME}-${ENVIRONMENT}`;

    const callbackURl = (COGNITO_CALLBACK_URL) ? COGNITO_CALLBACK_URL : 'http://localhost:3000';

    this.userPoolV2 = new UserPool(this,  `${this.projectEnv}-user-pool-v2`, {
      selfSignUpEnabled: ENVIRONMENT === 'prod',
      accountRecovery: AccountRecovery.EMAIL_ONLY,
      userVerification: {
        emailStyle: VerificationEmailStyle.CODE,
      },
      autoVerify: {
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
          mutable: true,
        },
      },
      signInAliases: {
        username:  true,
        email: true,
      }
    });

    const clientWritteAttributes = new ClientAttributes()
      .withStandardAttributes({
        fullname: true,
        email: true,
        nickname: true,
        profilePicture: true,
      });

    const clientReadAttributes = new ClientAttributes()
      .withStandardAttributes({
        emailVerified: true,
        email: true,
      });

    this.userPoolClientV2 = new UserPoolClient(this,`${this.projectEnv}-user-pool-client-v2`, {
      userPool: this.userPoolV2,
      authFlows: {
        userPassword: true,
        userSrp: true,
      },
      generateSecret: true,
      preventUserExistenceErrors: true,
      writeAttributes: clientWritteAttributes,
      readAttributes: clientReadAttributes,
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE],
        callbackUrls: [`${callbackURl}/auth`],
        logoutUrls: [`${callbackURl}/logout`],
      },
    });

    this.userPoolV2.addDomain(`${PROJECT_NAME}-domain`,{
      cognitoDomain: { domainPrefix: `${this.projectEnv}-v2` }
    });

    new CfnOutput(this,'CognitoUspLoginUrlV2',{
      value: `https://${this.projectEnv}-v2.auth.${REGION}.amazoncognito.com/login?response_type=code&client_id=${this.userPoolClientV2.userPoolClientId}&redirect_uri=${callbackURl}/auth`
    });

    new CfnOutput(this,'CognitoURLV2',{
      value: `https://${this.projectEnv}.auth.${REGION}.amazoncognito.com`
    });

    addTags(this.userPoolV2,'project',`${PROJECT_NAME}`);

    this.createCognitoPreSignUpLambdaFunction();
  }

  private createCognitoPreSignUpLambdaFunction(): void {
    const lambdaFunction = new Function(this, 'cognitoPreSignUp', {
      runtime: Runtime.PYTHON_3_11,
      functionName: 'cognito-pre-sign-up',
      handler: 'src.handler.lambda_handler',
      code: Code.fromAsset('./services/cognito_pre_sign_up'),
      environment: {
        LOG_LEVEL: `${ENVIRONMENT === 'prod' ? 'INFO' : 'DEBUG'}`,
      },
      logRetention: ENVIRONMENT === 'prod' ? RetentionDays.ONE_YEAR : RetentionDays.ONE_DAY,
    });

// least privileges principle
    const lambdaPolicy = new PolicyStatement({
      actions: ['cognito-idp:ListUsers'],
      resources: [this.userPoolV2.userPoolArn],
      effect: Effect.ALLOW,
    });

    lambdaFunction.addToRolePolicy(lambdaPolicy);

    const layerArn = StringParameter.valueForStringParameter(this, `${PROJECT_NAME}-${ENVIRONMENT}-lambda-layer-parameter`);

    const newLayerVersion: ILayerVersion = LayerVersion.fromLayerVersionArn(this, 'lambda-layer-common', layerArn);

    lambdaFunction.addLayers(newLayerVersion);

    lambdaFunction.node.addDependency(this.userPoolV2);

    addTags(lambdaFunction, 'project', `${PROJECT_NAME}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Migrating Existing Users

Now another question arises: what to do with the users who were already registered in the previous User Pool?

In my case, there were only 5 users, so I chose the simplest solution:

  • Manually re-invite each one with their new data.
  • Manually update the userIDs in the DynamoDB table.

Conclusion

With this solution, I managed to solve the user duplication issues and ensure consistent data management. I also learned that:

  • Setting the email as the sign-in alias is crucial to avoid similar issues.
  • Cognito’s Pre-Signup Triggers offer great flexibility to customize registration logic.
  • Planning and testing are essential, especially when there are already users in production.

Now the system is more robust and ready to scale without duplication issues.

If you want to store your records you can join LiftWiz

Top comments (0)