DEV Community

Matt Marks 🐣
Matt Marks 🐣

Posted on • Updated on

Enforcing attribute uniqueness in Cognito with AWS Amplify and React

In this walkthrough, you'll learn how create a PreSignUp Lambda for Cognito in AWS Amplify. When a user signs up with email as an optional attribute, we'll fire up our PreSignUp trigger to search Cognito for users who already signed up with that email attribute.

What you need to get started

  1. amplify-cli
  2. npx

That's it, Lezzzgo!

$  npx create-react-app pre-signup
$  cd pre-signup
$  yarn add aws-amplify
$  yarn add aws-amplify-react
Enter fullscreen mode Exit fullscreen mode



Next we'll intialize amplify

$  amplify init
? Enter a name for the project: presignup
? Enter a name for the environment: dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use: default
Enter fullscreen mode Exit fullscreen mode



After initializing our project, we'll add authentication. When we run amplify add auth we'll do a manual configuration so that we can add our pre-signup trigger. Use the same configurations I've listed below.

$  amplify add auth
 Do you want to use the default authentication and security configuration? Manual configuration
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls 
 (Enables per-user Storage features for images or other content, Analytics, and more)
 Please provide a friendly name for your resource that will be used to label this category in the project: presignup9aa404bb9aa404bb
 Please enter a name for your identity pool. presignup9aa404bb_identitypool_9aa404bb
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
 Do you want to enable 3rd party authentication providers in your identity pool? No
 Please provide a name for your user pool: presignup9aa404bb_userpool_9aa404bb
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Email and Phone Number
 Do you want to add User Pool Groups? No
 Do you want to add an admin queries API? No
 Multifactor authentication (MFA) user login options: OPTIONAL (Individual users can use MFA)
 For user login, select the MFA types: (Press <space> to select, <a> to toggle all, <i> to invert selection)SMS Text Message
 Please specify an SMS authentication message: Your authentication code is {####}
 Email based user registration/forgot password: Disabled (Uses SMS/TOTP as an alternative)
 Please specify an SMS verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections. 
 What attributes are required for signing up? 
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? Yes
 Specify read attributes: Email, Phone Number, Preferred Username, Email Verified?, Phone Number Verified?
 Specify write attributes: Email, Phone Number, Preferred Username
 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? Yes
? Which triggers do you want to enable for Cognito Pre Sign-up
? What functionality do you want to use for Pre Sign-up Create your own module
Succesfully added the Lambda function locally
? Do you want to edit your custom function now? Yes
Please edit the file in your editor: Desktop/pre-signup/amplify/backend/function/presignup9aa404bb9aa404bbPreSignup/src/custom.js
? Press enter to continue 
Successfully added resource presignup9aa404bb9aa404bb locally
Enter fullscreen mode Exit fullscreen mode

Whooooakay, still with me πŸ˜…

Now we're gonna dive into the fun stuff!

In the prompt above, it will ask you if you'd like to edit your function. It will open up the custom.js file at /amplify/backend/function/<lambda-name>/custom.js. At this point you can delete the custom file and add the following code to /amplify/backend/function/<lambda-name>/index.js

exports.handler = async (event, context, callback) => {
  console.log({event}, event.request, event.request.userAttributes)
  callback(null, event)
}

Enter fullscreen mode Exit fullscreen mode

A little side note
With a PreSignUp trigger, we must return the original event back to Cognito after we add our custom logic. To demonstrate this, we’re going to deploy the code below with some console.logs of the event data, so you can see what to expect.

$  amplify push
Enter fullscreen mode Exit fullscreen mode

While that's deploying our resources to the cloud, let's add some Amplify Auth configurations to App.js

import React from "react";
import Auth from "@aws-amplify/auth";
import { withAuthenticator } from "aws-amplify-react";

import config from "./aws-exports";

Auth.configure(config);

const App = (props) => {
  return (
    <div style={{ color: "white", fontSize: 13}}>
      Wooohoooo, Succcessfully signed up user with username
    </div>)
};

  export default withAuthenticator(App, {
  signUpConfig: {
    signUpFields: [{ key: 'phone_number', required: false }]
  }
});
Enter fullscreen mode Exit fullscreen mode

After amplify push is finished, run yarn start

You'll be presented with a SignIn screen. Click create account and sign up with the following credentials (do not fill in phone number field, fill in username as phone number)

Username: +1111111111
Password: Password1@
Email: example@example.com
Enter fullscreen mode Exit fullscreen mode

Alt Text

If you're presented with the Confirm Sign Up page, congratulations! Our PreSignUp Lambda forwarded the event correctly and Cognito was able to continue with SignUp. Now try signing up again, but this time with the following credentials:

Username: +2222222222
Password: Password1@
Email: example@example.com
Enter fullscreen mode Exit fullscreen mode

Ahhh, just as we expected. Cognito allowed us to sign up with the same email when used as an optional attribute. Cognito is only enforcing uniqueness for username, which is a phone number.

I would take this time to check the cloudwatch logs for your lambda as we'll be using the event data for something later πŸ˜‰ (hint, hint: quick way to find the logs is to find your presignup lambda inside the lambda console, click on monitoring, then view in cloudwatch)

What our logs should look like:

2020-03-02T20:37:56.850Z    d740d6f7-71be-4634-a36b-23d916e1cdb9    INFO    
{ event:
   { version: '1',
     region: 'us-east-1',
     userPoolId: 'us-east-1_HQBTO8LlF',
     userName: '5f1fa3d5-acfe-4e65-80b0-7e5753c83c25',
     callerContext:  { 
       awsSdkVersion: 'aws-sdk-unknown-unknown',
       clientId: '2b7j54vvm9a7c1inqum0nkq4v' 
     },
     triggerSource: 'PreSignUp_SignUp',
     request: { 
       userAttributes:  { 
         phone_number: '+11111111111', 
         email: 'example@example.com' 
       },
       validationData: null 
      },
      response: { 
        autoConfirmUser: false,
        autoVerifyEmail: false,
        autoVerifyPhone: false 
      } 
   } 
} 

Enter fullscreen mode Exit fullscreen mode

Now we've seen the problem in action, we're going to add the code for our PreSignUp lambda. It will looks for users with email that matches the one from provided from user signup. Replace the code at /amplify/backend/function/<lambda-name>/index.js with the following:

const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';

const identity = new AWS.CognitoIdentityServiceProvider();

exports.handler = async (event, context, callback) => {
  if (event.request.userAttributes.email) {
    const {email} = event.request.userAttributes
    const userParams = {
      UserPoolId: event.userPoolId,
      AttributesToGet: ['email'],
      Filter: `email = \"${email}\"`,
      Limit: 1,
    };
    try {
      const {Users} = await identity.listUsers(userParams).promise();
      console.log({Users})
      if (Users && Users.length > 0) {
          callback('EmailExistsException', null);
      } else {
        callback(null, event);
      }
    } catch (error) {
      console.log({error}, JSON.stringify(error))
      callback({error}, null);
    }
  } else {
    callback('MissingParameters', null);
  }
};

Enter fullscreen mode Exit fullscreen mode

What's happening here is that we're making a request for all users in our user pool and filtering it down to users with the email we've provided. If a user exists, we return EmailExistsException error message.

Protip: With Amplify, we can test our functions locally 🀯

Remember those cloudwatch logs from earlier? They're about to come in handy. Copy the event data into /amplify/backend/function/<lambda-name>/events.json

{
  "version": "1",
  "region": "us-east-1",
  "userPoolId": "us-east-1_HQBTO8LlF",
  "userName": "5f1fa3d5-acfe-4e65-80b0-7e5753c83c25",
  "callerContext": {
    "awsSdkVersion": "aws-sdk-unknown-unknown",
    "clientId": "2b7j54vvm9a7c1inqum0nkq4v"
  },
  "triggerSource": "PreSignUp_SignUp",
  "request": {
    "userAttributes": {
      "phone_number": "+1111111111",
      "email": "example@example.com"
    },
    "validationData": null
  },
  "response": {
    "autoConfirmUser": false,
    "autoVerifyEmail": false,
    "autoVerifyPhone": false
  }
}

Enter fullscreen mode Exit fullscreen mode

Run the following from your terminal

$  amplify function invoke <your-lambda-name> 
Enter fullscreen mode Exit fullscreen mode

Alt Text

Niiiiiiiice. Although our lambda works locally, it will need permissions to call Cognito:Identity-ServiceProvider.listUsers() when it's deployed. We're gonna step into our lambda's cloudformation template and add a Policy. Go to /amplify/backend/function/<lambda-name>/<lambda-name>-cloudformation-template.json

Nested under "Resources" you'll see the following:

"LambdaExecutionRole": {
  "Type": "AWS::IAM::Role",
  ...
},
"lambdaexecutionpolicy": {
  "DependsOn": [
    "LambdaExecutionRole"
   ],
  "Type": "AWS::IAM::Policy",
  ...
}
Enter fullscreen mode Exit fullscreen mode

We're going to add a policy between the existing role and policy


"LambdaExecutionRole": {
  "Type": "AWS::IAM::Role",
  ...
},
"lambalistuserspolicy": {
  "DependsOn": [
    "LambdaExecutionRole"
  ],
  "Type": "AWS::IAM::Policy",
  "Properties": {
    "PolicyName": "lambda-list-users-policy",
    "Roles": [
      {
        "Ref": "LambdaExecutionRole"
      }
    ],
    "PolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "cognito-idp:ListUsers"
          ],
          "Resource": {
            "Fn::Sub": [
              "arn:aws:cognito-idp:${region}:${account}:userpool/us-east-1_HQBTO8LlF",
              {
                "region": {
                  "Ref": "AWS::Region"
                },
                "account": {
                  "Ref": "AWS::AccountId"
                }
              }
            ]
          }
        }
      ]
    }
  }
},
"lambdaexecutionpolicy": {
  "DependsOn": [
    "LambdaExecutionRole"
   ],
  "Type": "AWS::IAM::Policy",
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now it's time to deploy our pride and joy πŸ₯³

amplify function push
Enter fullscreen mode Exit fullscreen mode

Let's try signing up with any username and an email of example@example:
Alt Text

Ohhhh, the sweet, sweet taste of a fully functioning PreSignUp lambda that enforces email uniqeuness πŸ™Œ πŸ™Œ πŸ™Œ πŸ™Œ πŸ™Œ πŸ™Œ

To learn more about the resources used in this walkthrough, check out the following:
Cognito List Users
Amplify Authentication
Pre Sign-up Lambda Trigger

Follow me on Twitter @andthensumm

Top comments (9)

Collapse
 
ignaciolarranaga profile image
Ignacio LarraΓ±aga

Did you find any way to make pool id parametric?

Collapse
 
andthensumm profile image
Matt Marks 🐣

If I'm understanding you correctly, the user pool id is automatically passed into any cognito trigger. Every trigger event will have the userPoolId field available

Collapse
 
mgiorgigithub profile image
mgiorgi • Edited

Hi, I use Lambda function with React Native (Auth.signUp) and the error message returned is "PreSignUp failed with error [object Object].", can i pass the custom message only for this specific lambda like "SignUp failed, your email il already user"? I don't see EmailExistsException string... Thanks

Collapse
 
devorein profile image
Safwan Shaheer

You are a life saver. Thank you so much.

Collapse
 
andthensumm profile image
Matt Marks 🐣

heyooo, always love that, glad it helped

Collapse
 
sudharshan1409 profile image
Sudharshan V

Really much needed thing! Thanks a lot

Collapse
 
asitprakash profile image
Asit Prakash

Worked like a charm..thanks

Collapse
 
andthensumm profile image
Matt Marks 🐣

Ayyyy love to hear that!

Collapse
 
artiom_nehoda_e764ea83350 profile image
Artiom Nehoda

Hi, thank you for this article. But how about performance of listUsers? how slow will the request be if the app has millions of users?