Amazon Cognito でメールアドレス変更する際の注意点
Amazon Cognito のメールアドレス変更には以下のような問題があります。
* 新しいメールアドレスを申請して、メールで検証をする前にその新しいメールでログインできてしまう
* 逆に検証が成功していないのに古いメールアドレスでログインできない
* つまり間違ったメールアドレスをリクエストするとその時点で詰む(ユーザーロックというらしい)
これを回避する方法は上の記事やissueにいくつか書かれています。
以下では、個人的にベターと思う方法を書きました。
今回Golangのサーバーで実装していますが、同じロジックでlambdaでの実装もできると思います。
基本的な方針は以下を参考にしました。
メールアドレス変更依頼
リクエストを受け取る部分等は省いて、Cognitoにリクエストする部分だけ書きます。
var client *cognitoidentityprovider.CognitoIdentityProvider
var userPoolID string
func RequestEmailUpdate(
currentEmail string,
requestedEmail string,
accessToken string,
) error {
// ユーザーに検証コードを送信するためにUpdateUserAttributesを呼ぶ
updateInput := &cognitoidentityprovider.UpdateUserAttributesInput{
AccessToken: aws.String(accessToken),
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(requestedEmail),
},
},
}
_, err := client.UpdateUserAttributes(updateInput)
if err != nil {
return err
}
// 検証コードを確認する前に"email"が更新されてしまうので、AdminUpdateUserAttributesで戻す
// 更新予定のemailを"custom:requested_email"にセットしておく
adminUpdateInput := &cognitoidentityprovider.AdminUpdateUserAttributesInput{
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(currentEmail),
},
{
Name: aws.String("email_verified"),
Value: aws.String("true"),
},
{
Name: aws.String("custom:requested_email"),
Value: aws.String(requestedEmail),
},
},
UserPoolId: aws.String(userPoolID),
Username: aws.String(requestedEmail),
}
_, err = client.AdminUpdateUserAttributes(adminUpdateInput)
if err != nil {
return err
}
return nil
}
メールアドレス変更確認
func ConfirmEmailUpdate(
currentEmail string,
accessToken string,
code string,
) error {
// "custom:requested_email"を取得
usr, err := client.AdminGetUser(&cognitoidentityprovider.AdminGetUserInput{
UserPoolId: aws.String(userPoolID),
Username: aws.String(currentEmail),
})
if err != nil {
return err
}
var requestedEmail string
for _, attr := range usr.UserAttributes {
if aws.StringValue(attr.Name) == "custom:requested_email" {
requestedEmail = aws.StringValue(attr.Value)
break
}
}
// 検証コードを確認する
verifyInput := &cognitoidentityprovider.VerifyUserAttributeInput{
AccessToken: aws.String(accessToken),
AttributeName: aws.String(emailAttribute),
Code: aws.String(code),
}
_, err = client.VerifyUserAttribute(verifyInput)
if err != nil {
return err
}
// 検証コードが確認できたので"email"を更新する
// 使い終わった"custom:requested_email"を削除する
adminUpdateInput := &cognitoidentityprovider.AdminUpdateUserAttributesInput{
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(requestedEmail),
},
{
Name: aws.String("email_verified"),
Value: aws.String("true"),
},
{
Name: aws.String("custom:requested_email"),
Value: aws.String(""),
},
},
UserPoolId: aws.String(userPoolID),
Username: aws.String(currentEmail),
}
_, err = client.AdminUpdateUserAttributes(adminUpdateInput)
if err != nil {
return err
}
return nil
}
Cognitoのカスタム属性に "custom:requested_email"
を追加し、読み取り専用にしておきます。
変更予定のメールアドレスに来たコードを検証するまでは、 "custom:requested_email"
に値が追加されただけでそれ以外の情報は一切変わりません。そのため、コードを検証するまでは変更前のメールアドレスでログインできます。
この流れだと「検証コードが送られるメールアドレス」と「検証後に更新されるメールアドレス」が必ず一致するので、これが一番ベターかなと思います。
一時的にしか使わない値をclaimに保存するのは少しモヤっとしますが、ワークアラウンドなので仕方ないと思うことにします。
※2022/05/11追記
cognitoの仕様変更?で VerifyUserAttribute
が成功した時にメールアドレスが変更予定の値に変更されるようになったようです。requested_email
に保存する必要がなくなりましたが、検証する前に変更される挙動自体は変わっていないので、以下のようにしました。
var client *cognitoidentityprovider.CognitoIdentityProvider
var userPoolID string
func RequestEmailUpdate(
currentEmail string,
requestedEmail string,
accessToken string,
) error {
// ユーザーに検証コードを送信するためにUpdateUserAttributesを呼ぶ
updateInput := &cognitoidentityprovider.UpdateUserAttributesInput{
AccessToken: aws.String(accessToken),
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(requestedEmail),
},
},
}
_, err := client.UpdateUserAttributes(updateInput)
if err != nil {
return err
}
// 検証コードを確認する前に"email"が更新されてしまうので、AdminUpdateUserAttributesで戻す
adminUpdateInput := &cognitoidentityprovider.AdminUpdateUserAttributesInput{
UserAttributes: []*cognitoidentityprovider.AttributeType{
{
Name: aws.String("email"),
Value: aws.String(currentEmail),
},
{
Name: aws.String("email_verified"),
Value: aws.String("true"),
},
},
UserPoolId: aws.String(userPoolID),
Username: aws.String(requestedEmail),
}
_, err = client.AdminUpdateUserAttributes(adminUpdateInput)
if err != nil {
return err
}
return nil
}
func ConfirmEmailUpdate(
currentEmail string,
accessToken string,
code string,
) error {
// 検証コードを確認する
verifyInput := &cognitoidentityprovider.VerifyUserAttributeInput{
AccessToken: aws.String(accessToken),
AttributeName: aws.String(emailAttribute),
Code: aws.String(code),
}
_, err = client.VerifyUserAttribute(verifyInput)
if err != nil {
return err
}
return nil
}