# Deploy Cognito Triggers Using Serverless Framework

Recently, I posted an article about [pre signup triggers for AWS Cognito user pools](https://kelvinmwinuka.com/pre-signup-validation-on-aws-cognito). The article went over setting up pre signup validation using a lambda function.

In this article, I'm going to demonstrate how to achieve the same goal, but using the serverless framework instead of the AWS console to develop and deploy the lambdas and policies.

## Setup

First install the serverless framework using the following command: `npm install -g serverless`.

Once serverless is installed, create a project with the following command; `serverless create --template aws-nodejs -n cognito-triggers`.

With the command above, we're creating a nodejs serverless project intended to be hosted on the AWS cloud platform. We then pass the name of `cognito-triggers` for the project.

Feel free to use whatever language you want in this project, just be sure to follow your language's package installation and build steps where necessary.

## Implementation

First, let's create a `.env` file at the root of the project with the following content:

```
COGNITO_USER_POOL_NAME=<user_pool_name>
COGNITO_USER_POOL_ID=<user_pool_id>
COGNITO_USER_POOL_ARN=<user_pool_arn>
REGION=us-west-2
```
If you're deploying to multiple environments (e.g. testing, staging, production) then you should have multiple env files in your project named in the format `.env.{stage}`. So the production env file will be `.env.production`. 

For this project, we will stick to one env file for the sake of simplicity.

Let's install a few packages we're going to need for this project with the following command: `npm install @aws-sdk/client-cognito-identity-provider dotenv serverless-dotenv-plugin`

We will need the `dotenv` and `serverless-dotenv-plugin` for loading environment variables. We also need the `serverless-offline` package if we want to invoke our lambda functions locally. We won't be going into that in this article but you can install it with `npm install -D serverless-offline`.

You'll notice that there's a `serveless.yml` file at the root of the project. Open the file and add the following code:

```yaml
service: cognito-triggers

useDotenv: true

plugins:
  - serverless-dotenv-plugin
  - serverless-offline

frameworkVersion: '3'
```
In the code above, we're setting the service name, setting `useDotenv: true` to allow us to load environment variables. 

The plugins section has 2 plugins: 
1. `serverless-dotenv-plugin` for loading environment variables.
2. `serverless-offline` for running the project on the local machine (in case you want to invoke the functions locally).

Now let's define the provider:

```yaml
provider:
  name: aws
  runtime: nodejs14.x
  stage: ${opt:stage, 'dev'}
  region: ${env:REGION}
  profile: default
  iam:
    role:
      statements:
        - Effect: 'Allow'
          Action: 'cognito-idp:ListUsers'
          Resource: '${env:COGNITO_USER_POOL_ARN}'

package:
  patterns:
    - '!.gitignore'
```

We set the provider name to `aws` because we're using the AWS platform. Our runtime of choice is nodejs14.x. Be sure to use a runtime that's available for the platform you're deploying to.

When setting the stage, we prefer the provided `--stage` in the command options. If one is not provided, default to `dev`. This will dictate which environment file to use when running the service offline or deploying. For example `sls offline --stage staging` will run the service on your local machine using `.env.staging` while `sls deploy --stage production` will deploy the service with the `.env.production` file.

Now for the interesting part, the actual lambda functions! At the top level of the `serverless.yml` file right below the provider section, create a function section with the following code:

```yaml
functions:
  pre_signup:
    handler: ./src/pre_signup.handler
    events:
      - cognitoUserPool:
          pool: ${env:COGNITO_USER_POOL_NAME}
          trigger: PreSignUp
          existing: true
```
The `functions` section is where we declare the lambda functions in our service. Here we have a `pre_signup` function.

The `handler` property of the function points to the handler function exported by a js file. Make sure the path matches the location of your file. Here we have the file in an `src` folder that's located at the root of the project.

The `events` property determines what kind of events can trigger this lambda function. This can be an HTTP request through an API gateway, or in our case, a Cognito signup to a user pool.

If you already have an existing user pool, you have to set the `existing` property to `true` for the `cognitoUserPool` while specifying the user pool's name in the `pool` property.

Let's create the js function to handle all the pre signup logic.

Create an `src` folder at the root of the project and then create a file called `pre_signup.js` within that folder. The file will have the following content:

```javascript
'use strict';
require("dotenv").config({});

const { COGNITO_USER_POOL_ID } = process.env;

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

module.exports.handler = async (event, context, callback) => {
  const client = new CognitoIdentityProviderClient();
  
  const listUsersCommand = new ListUsersCommand({
    UserPoolId: COGNITO_USER_POOL_ID,
    Filter: `email = "${event.request.userAttributes.email}"`
  });

  const result = await client.send(listUsersCommand);

  if (result.Users.length > 0) return callback(new Error("Email is already in use."), event);

  callback(null, event);
};

```

This code is very familiar if you read my previous article on [Pre Signup Validation on AWS Cognito](https://kelvinmwinuka.com/pre-signup-validation-on-aws-cognito). Basically, we're listing the users in our user pool that have the same email address as the one provided in this signup attempt. If we have more than 0, then throw an error stating that the email address is already in use.

Notice, we're exporting a `handler` function. This is the function that we reference in the `serverless.yml` file.

While we're here, let's create another function to edit the messages sent to the user's email address. Cognito user pools have a `Custom message` trigger that allows us to intercept a message before it's sent and edit its content.

In the `functions` section of `serverless.yml`, create a function called `custom_message` with the following properties:

```yaml
  custom_message:
    handler: ./src/custom_message.handler
    events:
      - cognitoUserPool:
          pool: ${env:COGNITO_USER_POOL_NAME}
          trigger: CustomMessage
          existing: true
```
It is identical to the pre_signup functions except it's referencing a different handler and hooking into the CustomMessage trigger.

In the `src` folder create a `custom_message.js` file with the following content:

```javascript
'use strict';
require("dotenv").config({});

module.exports.handler = async (event, context, callback) => {
  switch(event.triggerSource) {
    case "CustomMessage_SignUp":
      event.response.smsMessage = `Hi ${event.userName}, your signup code is ${event.request.codeParameter}`;
      event.response.emailSubject = `Your registration code`;
      event.response.emailMessage = `Hi ${event.userName}, your signup code is ${event.request.codeParameter}`;
      break;
    case "CustomMessage_ForgotPassword":
      event.response.smsMessage = `Hi ${event.userName}, your password reset code is ${event.request.codeParameter}. If you did not request this code, ignore this message. Please DO NOT share this code with anyone.`;
      event.response.emailSubject = `Your password reset code`;
      event.response.emailMessage = `Hi ${event.userName}, your password reset code is ${event.request.codeParameter}. If you did not request this code, ignore this email. Please DO NOT share this code with anyone.`;
      break;
    case "CustomMessage_ResendCode":
      event.response.smsMessage = `Hi ${event.userName}, your requested code is ${event.request.codeParameter}`;
      event.response.emailSubject = `Your requested code`;
      event.response.emailMessage = `Hi ${event.userName}, your requested verification code is ${event.request.codeParameter}`;
      break;
    default:
      event.response.smsMessage = `Hi ${event.userName}, your requested code is ${event.request.codeParameter}`;
      event.response.emailSubject = `Your requested code`;
      event.response.emailMessage = `Hi ${event.userName}, your requested code is ${event.request.codeParameter}`;
  }
  callback(null, event);
}
```

The handler captures different message events and displays a relevant message depending on the message event. `CustomMessage_SignUp` is the trigger source when the signup verification email is triggered, `CustomMessage_ForgotPassword` for the password reset email and `CustomMessage_ResendCode` when a manual code request is triggered (e.g attempting to log in when unconfirmed). 

You can find more information about the different trigger sources [here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html).

The event object for this trigger looks like this: 

```json
{
  version: '1',
  region: 'us-west-2',
  userPoolId: '<user_pool_id>',
  userName: '<username>',
  callerContext: {
    awsSdkVersion: 'aws-sdk-js-3.58.0',
    clientId: '<client_id>'
  },
  triggerSource: 'CustomMessage_SignUp',
  request: {
    userAttributes: {
      sub: 'd98dad2a-c2f3-4f97-bc49-a3ed3c81f27a',
      email_verified: 'false',
      'cognito:user_status': 'UNCONFIRMED',
      email: '<user_email_address>'
    },
    codeParameter: '{####}',
    linkParameter: '{##Click Here##}',
    usernameParameter: null
  },
  response: { smsMessage: null, emailMessage: null, emailSubject: null }
}
```
Make sure you include the `codeParameter` in your custom message.

## Deployment

To deploy the app, run: `sls deploy`. If you are deploying to a specific environment, specify it with the `--stage` option (e.g. `sls deploy --stage staging` or `sls deploy --stage production`).

If you'd like to remove the deployed service, run `sls remove` or `sls remove --stage <stage>` and the service along with any resources you might have created in this service will be destroyed.
