Table of contents
Recently, I posted an article about pre signup triggers for AWS Cognito user pools. 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:
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:
serverless-dotenv-plugin
for loading environment variables.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:
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:
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:
'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. 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:
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:
'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.
The event object for this trigger looks like this:
{
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.