Enforcing attribute uniqueness in Cognito with AWS Amplify and React
Tags: awsamplify, react, awscognito, awslambda
Published: 2020-05-27 16:27:23



In this walkthrough, you'll learn how create a PreSignUp Lambda for Cognito in [AWS Amplify](https://aws-amplify.github.io/).  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](https://github.com/aws-amplify/amplify-cli)
2. [npx](https://github.com/npm/npx#readme)

### That's it,  Lezzzgo!

```console
$  npx create-react-app pre-signup
$  cd pre-signup
$  yarn add aws-amplify
$  yarn add aws-amplify-react
```
<br>
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
```

<br>
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.

```monocode
$  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
```

### 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`

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

```

**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
```

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

```javascript
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 }]
  }
});
```

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
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mec5zfsqfo9gyx8kbtaa.png)

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
```

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:
```json
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 
      } 
   } 
} 

```

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:

```javascript
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);
  }
};

```

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`
```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
  }
}

```

Run the following from your terminal
```
$  amplify function invoke <your-lambda-name> 
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/57qa9muxxb5s1ijr7jk5.gif)


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:

```json
"LambdaExecutionRole": {
  "Type": "AWS::IAM::Role",
  ...
},
"lambdaexecutionpolicy": {
  "DependsOn": [
    "LambdaExecutionRole"
   ],
  "Type": "AWS::IAM::Policy",
  ...
}
```

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

"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",
  ...
}
```

Now it's time to deploy our pride and joy 🥳
```
amplify function push
```

Let's try signing up with any username and an email of example@example:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sup4eettxavcueszyxb5.gif)


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](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ListUsers.html)
[Amplify Authentication](https://aws-amplify.github.io/docs/js/authentication)
[Pre Sign-up Lambda Trigger](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html)

Follow me on Twitter [@andthensumm](https://twitter.com/andthensumm)