Let’s say you want to use the new awesome user management service provided by AWS. All nice and dandy, until you find that you can’t customize the email validation behaviour that much. I mean, yes, you can send HTML and you can add a custom domain, but that’s pretty much it. For example, I wanted to be able to redirect the user to my website after they validated their email and save the validatedAt timestamp in RDS.

I ended up using Cognito’s CustomMessage_SignUp event for altering the template and pointing the link to an API Gateway that did the actual validation, validatedAt save and the redirect.

In more detail, here’s the event handling. For the sake of simplicity this is not a valid HTML :)

if (event.triggerSource === 'CustomMessage_SignUp') {
    // Ensure that your message contains event.request.codeParameter. This is the placeholder for code that will be sent
    const { codeParameter, linkParameter } = event.request;
    const { userName } = event;
    const { clientId } = event.callerContext;
    const { email } = event.request.userAttributes;
    const url = `https://yourdomain.com/userEmailVerification/?code=${codeParameter}&username=${userName}&clientId=${clientId}&email=${email}`;
    event.response.emailSubject = 'Please verify your email addresss'; 
    event.response.emailMessage = `
    <!DOCTYPE html>
      <html>
        <a
            href="${url}"
            target="_blank"
            style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;"
            >Verify</a
        >
        <div style="display: none">${linkParameter}</div>
      </html>
    `;

    return event;
  }

The most important part is <div style="display: none">${linkParameter}</div>. Basically Cognito gives you this token that you need to add in your HTML and it will replace it with the validation URL that points to their hosted UI. Since that’s what I was aiming to avoid, I tried removing it all together but surprise, you can’t. You need to add it, even if it’s hidden, but it needs to be there. Another thing to keep in mind in the limitation for the emailMessage, you need to keep the text under 20000 characters, otherwise the email will not be sent.

The link https://yourdomain.com/userEmailVerification/ points to the above mentioned API Gateway and it’s Lambda function looks like this:

import { CognitoIdentityServiceProvider } from 'aws-sdk';
import { UserMapper } from '../database';

export const handler = async (event, context, callback) => {
  const { clientId, code, email, username } = event.queryStringParameters;
  const cognitoISP = new CognitoIdentityServiceProvider();
  const params = {
    ClientId: clientId,
    ConfirmationCode: code,
    Username: username,
  };

  try {
    await cognitoISP.confirmSignUp(params).promise();
    await UserMapper.verifyUser({ email: username });
    return callback(null, {
      statusCode: 302,
      headers: {
        Location: `https://mywebsite.com/login?verified=true&email=${email}`,
      },
    });
  } catch (e) {
    // woops, error, redirect but without verified=true
    return callback(null, {
      statusCode: 302,
      headers: {
        Location: `https://www.mywebsite.com/login?email=${email}`,
      },
    });
  }
};