Amazon Cognito でメールアドレス変更する際の注意点

Amazon Cognito のメールアドレス変更には以下のような問題があります。

Cognitoでメールアドレス編集するとログインできなくなる問題
* 新しいメールアドレスを申請して、メールで検証をする前にその新しいメールでログインできてしまう
* 逆に検証が成功していないのに古いメールアドレスでログインできない
* つまり間違ったメールアドレスをリクエストするとその時点で詰む(ユーザーロックというらしい)

これを回避する方法は上の記事やissueにいくつか書かれています。

以下では、個人的にベターと思う方法を書きました。

今回Golangのサーバーで実装していますが、同じロジックでlambdaでの実装もできると思います。

基本的な方針は以下を参考にしました。

Change email with AWS Cognito and Amplify - Jordizle

メールアドレス変更依頼

リクエストを受け取る部分等は省いて、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"を追加し、読み取り専用にしておきます。

----------2021-11-28-21.10.59

変更予定のメールアドレスに来たコードを検証するまでは、 "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
}