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
But I have logging setup and if I check the logs it totally doesn't align with behavior.
- Its being called as intended
- It doesnt error out
- 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

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
VerificationEmailStyleproperty (CodeorEmail).If you choose
Emaillike 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=getfromeventFor
VerificationEmailStyle.Codeit seems that theRequest.UsernameParameteris empty, so you can use theUserNameproperty on the event itself, or get it from user attributesYou can get these parameters from the event itself, at least in golang
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