AWS Cognito, overridden `CustomMessage_SignUp` sending the default message regardless

79 Views Asked by At

I wanted to have custom HTML emails when user signs up and gets the verification link, but for some reason its still sending the default email. Here is what I did to setup custom email verification email

Added the custom message trigger to my Cognito user pool

nstructor(scope: Construct, id: string, props: AuthStackProps) {
        super(scope, id, props);

        const signupInitiatedLambda = this.getPreSignupLambdaHandler(props);
        const signupConfirmationLambda = this.getPostConfirmationLambdaHandler(props);
        const customMessageLambda = this.getCustomMessageLambdaHandler(props);

        this.userPool = new UserPool(this, "UserPool", {
            selfSignUpEnabled: true,
            signInAliases: {
                email: true,
            },
            standardAttributes: {
                fullname: {
                    mutable: true,
                    required: true,
                },
                email: {
                    mutable: true,
                    required: true,
                },
            },
            userVerification: {
                emailStyle: VerificationEmailStyle.LINK
            },
            passwordPolicy: getPasswordPolicy(props.deployEnv),
            lambdaTriggers: {
                postConfirmation: signupConfirmationLambda,
                preSignUp: signupInitiatedLambda,
                customMessage: customMessageLambda,
            },
        });

The lambda itself is defined as such

    getCustomMessageLambdaHandler(props: AuthStackProps) {
        const f = new lambda.DockerImageFunction(this, "CustomMessageLambda", {
            code: lambda.DockerImageCode.fromImageAsset(path.join(__dirname, "../../../"), {
                ignoreMode: IgnoreMode.DOCKER,
                file: "internal/functions/auth/signup/custom-message/Dockerfile",
                exclude: ["cdk"],
            }),
            logRetention: 5,
            environment: {
                ENVIRONMENT: props.deployEnv,
                ENDPOINT: isLocal(props.deployEnv) ? "http://localhost:3000" : `https://${getDomainName("dashboard", props.deployEnv)}`
            },
        })

        return f
    }

And the lambda itself is a golang lambda

func handler(ctx context.Context, event events.CognitoEventUserPoolsCustomMessage) (events.CognitoEventUserPoolsCustomMessage, error) {
    if event.TriggerSource == "CustomMessage_SignUp" {
        slog.InfoContext(ctx, "processing event", slog.Any("event", event))
        endpoint := os.Getenv("ENDPOINT")
        if endpoint == "" {
            panic("ENDPOINT env variable is not set")
        }

        codeParameter := event.Request.CodeParameter
        username := event.UserName

        baseURL := fmt.Sprintf("%s/auth/confirm-user", endpoint)
        queryParams := url.Values{
            "code":     {codeParameter},
            "username": {username},
        }
        link := fmt.Sprintf("%s?%s", baseURL, queryParams.Encode())

        slog.InfoContext(ctx, "link", slog.String("link", link))

        event.Response.EmailSubject = "Your verification link!"

        htmlTemplate := getHTMLTemplate(event.TriggerSource, map[string]string{
            "--LINK--": link,
        })

        event.Response.EmailMessage = htmlTemplate
    }

    fmt.Printf("Response: %+v\n", event.Response)

    return event, nil
}

func main() {
    lambda.Start(handler)
}

Using localstack and developing locally with this works fine, but when I went out to try it on real aws infra, then it stopped working. The email that I get is the default one

screenshot of the default email verification link

But I have logging setup and if I check the logs it totally doesn't align with behavior.

  1. Its being called as intended
  2. It doesnt error out
  3. It changes the email.Response as intended
2024/02/02 08:16:41 INFO processing event event="{CognitoEventUserPoolsHeader:{Version:1 TriggerSource:CustomMessage_SignUp Region:eu-central-1 UserPoolID:eu-centralxxxxaK7j CallerContext:{AWSSDKVersion:aws-sdk-js-3.499.0 ClientID:7bofuhbioxxxxx7m5b55d} UserName:d196f9f1-1xxxxxbe-2d5d0718a09f} Request:{UserAttributes:map[cognito:email_alias:[email protected] cognito:user_status:UNCONFIRMED email:[email protected] email_verified:false name:Nikola sub:d196f9f1-1xxxxxd5d0718a09f] CodeParameter:{####} UsernameParameter: ClientMetadata:map[]} Response:{SMSMessage: EmailMessage: EmailSubject:}}"

2024/02/02 08:16:41 INFO link link="https://dashxxxxxxxx.com/auth/confirm-user?code=%7B%23%23%23%23%7D&username=d196f9f1-18b8-4de0-b0be-2d5d0718a09f"

Response: {SMSMessage: EmailMessage:<!doctype html>
... a lot of html elements
</html>
EmailSubject:Your verification link!}
REPORT RequestId: 81dcfd77-5d2b-4ceb-ba57-5642adf58005 Duration: 6.07 ms Billed Duration: 1603 ms Memory Size: 128 MB Max Memory Used: 16 MB Init Duration: 1596.72 ms

It seems to be working fine, but not sure why its not sending the correct email, I have no idea how to debug this since its not erroring out and everything seems to be in order.

Also is it even possible to render HTML from this trigger? I cant set the content type so it defaults to plain/text

1

There are 1 best solutions below

0
Nikola-Milovic On

The solution seems to be the following.

To use custom messages you need to adhere and include some special text in your template that AWS will replace with real values. The text that has to be included depends on your VerificationEmailStyle property (Code or Email).

If you choose Email like I did before, you have to include {##Whatever Text you want##}, and it will include the button with the link to verify your email.

But if you want a custom link like I did, you should use Code (the default) style and use the {####} in your template to get the code.

With this you can get a custom link like https://yourdomain.com/verify?code={####}&username=getfromevent

For VerificationEmailStyle.Code it seems that the Request.UsernameParameter is empty, so you can use the UserName property on the event itself, or get it from user attributes

You can get these parameters from the event itself, at least in golang

type CognitoEventUserPoolsCustomMessageRequest struct {
    UserAttributes    map[string]interface{} `json:"userAttributes"`
    CodeParameter     string                 `json:"codeParameter"`
    UsernameParameter string                 `json:"usernameParameter"`
    ClientMetadata    map[string]string      `json:"clientMetadata"`
}

Do not url escape since it will mess up the expected text.

If you still receive the default email instead of your custom one ( and there are no errors in logs), that means its failing silently, and you should recheck that you have the necessary text in your template.

Relevant docs