ServerlessでCognitoのcustomMessageを設定する
Cognitoでサインアップ時に、CustomMessage Triggerで確認メールの中身を書き換えたかったんですが、Serverless Frameworkから設定しようとするとエラいハマりました。
日本語圏の情報がほとんどなかったので自分用に備忘録。
要件
- 開発環境
- Serverless Framework 2.31.0
- Node.js 12.16.3
- Backend
- Cognito
- Lambda
- API Gateway
- Frontend(本記事には関係ない)
- Vue.js
上記構成で、認証及びメールアドレスの確認を行う。
その際、Cognitoが送る確認メールをCustomMessageで書き換えたい。
試行錯誤の筋道
正解だけ欲しい人はこちら。
最初は以下のように書いていました。
service: MyApp
provider:
name: aws
runtime: nodejs12.x
stage: '${opt:stage,"dev"}'
region: ap-northeast-1
functions:
customMessage:
handler: customMessage.handler
events:
- cognitoUserPool:
pool: MyAppUserPool
trigger: CustomMessage
resources:
Resources:
MyAppUserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
AccountRecoverySetting:
RecoveryMechanisms:
- Name: 'verified_email'
Priority: 1
AutoVerifiedAttributes:
- 'email'
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AliasAttributes:
- 'email'
EmailConfiguration:
EmailSendingAccount: DEVELOPER
From: '********@example.com'
ReplyToEmailAddress: '********@example.com'
SourceArn: 'arn:aws:ses:********:identity/********@example.com'
EmailVerificationMessage: '{####}.'
EmailVerificationSubject: '認証コードの送信'
DeviceConfiguration:
ChallengeRequiredOnNewDevice: true
DeviceOnlyRememberedOnUserPrompt: true
Policies:
PasswordPolicy:
MinimumLength: 6
RequireLowercase: false
RequireNumbers: false
RequireSymbols: false
RequireUppercase: false
TemporaryPasswordValidityDays: 365
Schema:
- AttributeDataType: String
Mutable: true
Name: 'email'
Required: true
- AttributeDataType: String
Mutable: true
Name: 'phone_number'
Required: false
UsernameConfiguration:
CaseSensitive: true
UserPoolName: '${self:service}-UserPool'
VerificationMessageTemplate:
DefaultEmailOption: 'CONFIRM_WITH_CODE'
EmailMessage: '{####}.'
EmailSubject: '認証コードの送信'
MyAppUserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
Properties:
ClientName: 'MyAppUserPoolClient'
DefaultRedirectURI: 'https://www.example.com'
CallbackURLs: 'https://www.example.com'
LogoutURLs: 'https://www.example.com'
ExplicitAuthFlows:
- ALLOW_ADMIN_USER_PASSWORD_AUTH
- ALLOW_CUSTOM_AUTH
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- phone_number
RefreshTokenValidity: 10
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref MyAppUserPool
exports.handler = (event, context, callback) => {
if (event.userPoolId === 'process.env.COGNITO_USER_POOL_ID') {
if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
event.response.emailMessage = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to MyApp - ${event.userName}</title>
</head>
<body>
<p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}">認証を完了する</a></p>
</body>
</html>
`
}
}
callback(null, event)
}
UserPoolが二つできる問題
上記のままDeployするとUserPoolが二つ作られてしまいます。なんじゃそりゃ。
かなり理解に苦しみましたが、以下の挙動のようでした。
- functions内でCognitoのTriggerを設定すると、指定したPool名をCognitoUserPool{指定したPool名}のように置換されてから新規にUserPoolが作成される
- resource内で指定した通り、新規にリソースが作成される
ということで、現状の設定だと
- CognitoUserPoolMyAppUserPool
- MyAppUserPool
の二つのUserPoolが作成されます。その際、CustomMessageトリガーはCognitoUserPoolMyAppUserPoolのみに設定されますし、resource側で指定した細かい設定はMyUserPoolのみに適用されます。
これでは使い物になりません。
調べた結果、functions側で決めたUserPool名に対し、先頭に'CognitoUserPool'を追加してresources側のUserPool名とするのがお作法らしいです。functions側でUserPoolリソースを作成し、resource側でその設定をオーバーライドしている扱いみたいな。
...
functions:
customMessage:
handler: customMessage.handler
environment:
- COGNITO_USER_POOL_ID: !ref MyAppUserPool
+ COGNITO_USER_POOL_ID: !ref CognitouserPoolMyAppUserPool # リソース名が変わったので参照名を変更
events:
- cognitoUserPool:
pool: MyAppUserPool
trigger: CustomMessage
resources:
Resources:
- MyAppUserPool:
+ CognitouserPoolMyAppUserPool: # CognitoUserPool{functions側で指定したPool名}
Type: 'AWS::Cognito::UserPool'
...
...
MyAppUserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
...
...
RefreshTokenValidity: 10
SupportedIdentityProviders:
- COGNITO
- UserPoolId: !Ref MyAppUserPool
+ UserPoolId: !Ref CognitouserPoolMyAppUserPool # リソース名が変わったので参照名を変更
はい、上記はまだ動作しません。
循環参照問題
上記の修正を行なってDeployしようとすると、以下のようなエラーが出てCloudFormationのチェックが通らなくなっていると思います。
リソースの作成主体がfunctions内のcustomMessageだから、その中でCognitoUserPoolMyAppUserPoolを参照しようとすると、自分の中での循環参照になるみたいです。
元々この環境変数は、Handler内部でTriggerが渡してきたUserPoolIdと環境変数経由のUserPoolIdを比較して複数のUserPoolから任意のUserPoolを選択する機能のためだったのですが、今回はUserPool一つで運用するため単純に機能を削減しました。
...
functions:
customMessage:
handler: customMessage.handler
environment:
- COGNITO_USER_POOL_ID: !ref CognitouserPoolMyAppUserPool
events:
- cognitoUserPool:
pool: MyAppUserPool
trigger: CustomMessage
resources:
Resources:
CognitouserPoolMyAppUserPool:
Type: 'AWS::Cognito::UserPool'
...
exports.handler = (event, context, callback) => {
- if (event.userPoolId === 'process.env.COGNITO_USER_POOL_ID') {
- if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
- event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
- event.response.emailMessage = `
- <!DOCTYPE html>
- <html>
- <head>
- <title>Welcome to MyApp - ${event.userName}</title>
- </head>
- <body>
- <p><a href="https://app.example.com/auth/confirm?us=${event.request.usernameParameter}&cp=${event.request.codeParameter}">認証を完了する</a></p>
- </body>
- </html>
- `
- }
- }
+ if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
+ event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
+ event.response.emailMessage = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Welcome to MyApp - ${event.userName}</title>
+ </head>
+ <body>
+ <p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}">認証を完了する</a></p>
+ </body>
+ </html>
+ `
+ }
callback(null, event)
}
なぜか本文が置換されない問題
ここまででDeployできるようになり、サインアップ時にメールが飛ぶようになりました。そのメールを見てみると、サブジェクトは置換されているのですが、本文が置換されていませんでした。
またまた調べたところ、CustomMessage_AdminCreateUserではevent.response.emailMessageにevent.request.usernameParameterとevent.request.codeParameterが含まれていないと機能しないようでした。この際、CloudWatchでもエラーメッセージが見当たらなかったので、問題の特定に苦労しました。
普通のSignUp処理で構わないので、CustomMessage_SignUpに変更して対応。
exports.handler = (event, context, callback) => {
- if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
+ if (event.triggerSource === 'CustomMessage_SignUp') {
event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
event.response.emailMessage = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to MyApp - ${event.userName}</title>
</head>
<body>
<p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}">認証を完了する</a></p>
</body>
</html>
`
}
callback(null, event)
}
exports.handler = (event, context, callback) => {
if (event.triggerSource === 'CustomMessage_AdminCreateUser') {
event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
event.response.emailMessage = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to MyApp - ${event.userName}</title>
</head>
<body>
- <p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}">認証を完了する</a></p>
+ <p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}&user=${event.request.usernameParameter}">認証を完了する</a></p>
</body>
</html>
`
}
callback(null, event)
}
正解
上記修正を施した正解は以下。
service: MyApp
provider:
name: aws
runtime: nodejs12.x
stage: '${opt:stage,"dev"}'
region: ap-northeast-1
functions:
customMessage:
handler: customMessage.handler
events:
- cognitoUserPool:
pool: MyAppUserPool
trigger: CustomMessage
resources:
Resources:
CognitoUserPoolMyAppUserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
AccountRecoverySetting:
RecoveryMechanisms:
- Name: 'verified_email'
Priority: 1
AutoVerifiedAttributes:
- 'email'
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AliasAttributes:
- 'email'
EmailConfiguration:
EmailSendingAccount: DEVELOPER
From: '********@example.com'
ReplyToEmailAddress: '********@example.com'
SourceArn: 'arn:aws:ses:********:identity/********@example.com'
EmailVerificationMessage: '{####}.'
EmailVerificationSubject: '認証コードの送信'
DeviceConfiguration:
ChallengeRequiredOnNewDevice: true
DeviceOnlyRememberedOnUserPrompt: true
Policies:
PasswordPolicy:
MinimumLength: 6
RequireLowercase: false
RequireNumbers: false
RequireSymbols: false
RequireUppercase: false
TemporaryPasswordValidityDays: 365
Schema:
- AttributeDataType: String
Mutable: true
Name: 'email'
Required: true
- AttributeDataType: String
Mutable: true
Name: 'phone_number'
Required: false
UsernameConfiguration:
CaseSensitive: true
UserPoolName: '${self:service}-UserPool'
VerificationMessageTemplate:
DefaultEmailOption: 'CONFIRM_WITH_CODE'
EmailMessage: '{####}.'
EmailSubject: '認証コードの送信'
MyAppUserPoolClient:
Type: 'AWS::Cognito::UserPoolClient'
Properties:
ClientName: 'MyAppUserPoolClient'
DefaultRedirectURI: 'https://www.example.com'
CallbackURLs: 'https://www.example.com'
LogoutURLs: 'https://www.example.com'
ExplicitAuthFlows:
- ALLOW_ADMIN_USER_PASSWORD_AUTH
- ALLOW_CUSTOM_AUTH
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- phone_number
RefreshTokenValidity: 10
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref CognitoUserPoolMyAppUserPool
exports.handler = (event, context, callback) => {
if (event.triggerSource === 'CustomMessage_SignUp') {
event.response.emailSubject = 'Welcome to MyAPP! Please verify your Email Adress.'
event.response.emailMessage = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to MyApp - ${event.userName}</title>
</head>
<body>
<p><a href="https://app.example.com/auth/confirm?code=${event.request.codeParameter}">認証を完了する</a></p>
</body>
</html>
`
}
callback(null, event)
}
結論
結局リファレンスに大抵のことは書いてあったのですが、直感に反する仕様だと、リファレンスの読み解き力が試されるなぁ、と。
参考
Discussion
助かりました!
これなんともしっくりこないですね。。
CFnだと違和感なく書けるんですかね?
これ existing って言うフィールドがちゃんとあるみたいですよ!