Pre Signup Validation on AWS Cognito
Tags: aws, cognito, lambda, node
Published: 2022-04-09 15:10:46

---
title: Pre Signup Validation on AWS Cognito
published: true
date: 2022-04-08 20:27:19 UTC
tags: aws,cognito,lambda,nodejs
canonical_url: https://kelvinmwinuka.com/pre-signup-validation-on-aws-cognito
---

In the last article, we leveraged AWS Cognito to handle user authentication in our application. There was a caveat with the setup, email sharing was allowed.

We want to make sure that we only have one user per email. We could build this logic into the sign-up handler in our app's backend but there are a couple of reasons why it's not the best practice.

1. The validation will only live in this particular backend. If we create another app that interacts with the same user pool, we will have to repeat this logic there too. 
2. We're adding bloat to the request handler, we could keep this handler clean by avoiding unnecessary code if we can.

Enter Cognito user pool triggers. We'll specifically look at the pre-signup trigger. This trigger is executed before Cognito's own signup validation with a couple of notable advantages:

1. We can add custom validation when registering a new user to the user pool. In our case, it's to check if the submitted email is already in use.
2. The logic is baked into the user pool. Any signup event for this user pool will be validated regardless of where it's coming from.

## Implementation

### Lambda

Make sure that the lambda is in the same region as the user pool. The following is the lambda's code:

```
const { CognitoIdentityProviderClient, ListUsersCommand } = require("@aws-sdk/client-cognito-identity-provider");

exports.handler = async (event, context, callback) => {
    // TODO implement
    const params = {
        UserPoolId: 'us-west-2_rxOJKcUKc',
        Filter: `email = "${event.request.userAttributes.email}"`
    };

    const client = new CognitoIdentityProviderClient();
    const listUsersCommand = new ListUsersCommand(params);

    const data = await client.send(listUsersCommand);

    if (data?.Users?.length > 0) {
        callback(new Error("Email is already taken"), event);
    } else {
        callback(null, event);
    }

};

```

Before we link this lambda to the user pool, we need to make sure it runs properly. In this example, I'm using the JavaScript v3 SDK. If we try to run this, the `@aws-sdk/client-cognito-identity-provider` module will not be found.

### Layer

This section is only applicable if you're using the v3 SDK at the time of writing this article. If you're not using the v3 SDK, you can skip ahead to the permissions section.

We need to create a lambda layer containing this package so we can use it in the lambda. Create a new project in your development environment. Navigate to the folder and run `npm init`.

Complete the setup process and then run `npm install @aws-sdk/client-cognito-identity-provider` to install the package.

Open the package.json file and add the following line to the `scripts` section:

```
{
  ...
  "scripts": {
    ...
    "build": "rm -rf nodejs && rm cognito-base-layer.zip && npm install && mkdir nodejs && mv node_modules nodejs && zip -r cognito-base-layer.zip . && cp cognito-base-layer.zip ~/Downloads"
  },
  ...
}

```

This is the build script for the layer. It does the following:

1. Delete the previous nodejs directory and cognito-base-layer.zip file from the previous build.
2. Install the packages.
3. Create a directory called `nodejs`.
4. Move the `node_modules` folder into `nodejs`.
5. Zip the current directory into a zip file called `cognito-base-layer.zip`.
6. Copy the zip file to the desired location (Optional).

If you're wondering why we move the node\_modules to a subfolder, this is where the lambda will look for the installed packages. If you only have node\_modules at the root, your packages will not be found.

When you're ready, run `npm run build`. A new zip file will be created. Now that we have the zip file, let's create a layer:

Go to the lambda service and click on `Layers`

![Screenshot 2022-04-08 at 20.39.39.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649439986758/qjvSi25qy.png)

Click on "Create layer"

![Screenshot 2022-04-08 at 20.40.04.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440050510/rsTDlhAXL.png)

Fill in the layer details:

![Screenshot 2022-04-08 at 20.41.24.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440168424/sJ2qSbvjT.png)

![Screenshot 2022-04-08 at 20.41.36.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440194310/jofk4cvfJ.png)

Name the layer whatever you like. In the upload section, upload the zip file that was generated in the previous build.

In the "compatible architectures" section, make sure to select the same architecture that your lambda is based on. In my case, it's x86\_64.

When you're done click "Create".

Let's add this layer to the lambda to allow it to make use of the package(s) in the layer.

Navigate to the lambda function and click on `Layers`:

![Screenshot 2022-04-08 at 20.54.19.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440744435/cqAVXOsnK.png)

Click on `Add a layer`.

![Screenshot 2022-04-08 at 20.54.51.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440803296/yHSHEdPjr.png)

Fill in the layer details:

![Screenshot 2022-04-08 at 20.55.41.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649440865215/WWm6eRLR4.png)

For the layer source, select `Custom layers`. In the custom layers dropdown menu, select the layer you just created. Once selected, you'll have the option to select a layer version.

Every time you make another upload on a layer, a new version is created and the old version is kept. This is to prevent breaking lambda functions that depend on the current version as each lambda has to specify which layer version they depend on.

Once you're done, click "Add".

That's it on layers, now our lambda can make use of the AWS JavaScript v3 SDK.

### Permissions

We're not done yet. This lambda currently doesn't have the correct permissions to list users from a Cognito user pool. Let's grant the correct permissions to the lambda function. First, take note of the lambda function's role.

You can find this in the `Configuration` tab of the lambda function:

![Screenshot 2022-04-08 at 21.16.33.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649441943542/7Zs8k24YA.png)

Navigate to the IAM Management Console and click on the `Policies` tab:

![Screenshot 2022-04-08 at 21.44.38.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649444372271/-KzOPnnmc.png)

Click on "Create Policy":

![Screenshot 2022-04-08 at 21.45.31.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649444419180/QRA6qxOns.png)

Click on the JSON tab and you should see a template like this:

```
{
    "Version": "2012-10-17",
    "Statement": []
}

```

In the statement array add the following code:

```
{
    "Effect": "Allow",
    "Action": "cognito-idp:ListUsers",
    "Resource": "arn:aws:cognito-idp:<region>:<account_id>:userpool/<userpool_id>"
}

```

The above policy statement allows us to list users from the specified Cognito user pool. Replace the `Resource` value with your user pool's ARN.

Now let's attach this policy to the lambda's role in order to allow the lambda to make use of it.

Navigate to "Roles" in the IAM Management Console:

![Screenshot 2022-04-08 at 22.18.40.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649445852126/fzCTkthtO.png)

Search for the lambda's role and click on it:

![Screenshot 2022-04-08 at 22.19.16.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649445892015/jnOf2WML4.png)

Click on the "Add permissions" dropdown and then click on "Attach policies":

![Screenshot 2022-04-08 at 22.19.39.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649445987267/l40guZRiU.png)

Search for the policy you just created, click on the check box and then click on "Attach policies" at the bottom of the page.

Now we have the right permissions and layers to execute this lambda. All we need to do now is set the lambda as a pre signup trigger for the user pool.

### Triggers

Navigate to the user pool's console and click on the "Triggers" tab:

![Screenshot 2022-04-08 at 22.33.00.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649446730895/nTmopAn1p.png)

Click on the Pre sign-up dropdown and select the lambda function we created:

![Screenshot 2022-04-08 at 22.33.34.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1649446847971/RcW2rHt81.png)

Click on "Save changes" at the bottom of the page.

## Result

That's all there is to the setup for our use case. Now we can go ahead and try to sign up with an email address that is already used in the user pool. Doing so should return an error that looks like this:

```
UserLambdaValidationException: PreSignUp failed with error Email is already taken.
...
{
    '$fault': 'client',
    '$metadata': {
        httpStatusCode: 400,
        requestId: '3bc8f968-cbf5-4960-857f-e48daa312870',
        extendedRequestId: undefined,
        cfId: undefined,
        attempts: 1,
        totalRetryDelay: 0
  },
  __type: 'UserLambdaValidationException'
}

```

## Bonus

You can do more with a pre sign-up trigger. The `event` object passed to the lambda function has a `response` property with the following structure:

"response": { "autoConfirmUser": "boolean", "autoVerifyPhone": "boolean" "autoVerifyEmail": "boolean" }

These all default to `false`. They can be set to `true` if you'd like to skip some parts of the default sign up flow. Before invoking the callback, you can add the following statements:

```
// Automatically set user's account status to CONFIRMED
event.response.autoConfirmUser = true

// Automatically set phone number as verified
event.response.autoVerifyPhone = true

// Automatically set email as verified
event.response.autoVerifyEmail = true

```