🤢

Cognitoでメールアドレス編集するとログインできなくなる問題

4 min read 1

ユーザーロックされる

こんにちはハトです。cognitoで詰まったので共有。

cognitoでメールアドレス変更機能を実装していた。すると以下のことに気づく。

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

cognitoはどうやらupdateUserAttributesが実行された時点でメールアドレスを新しいものに変更してしまっているみたい。ただし、Eメール検証はfalseになっている。
本来であれば、メールアドレス検証が終わったあとにメールアドレスが変更されていることを期待する。これでは動作がちぐはぐである。また上記の振る舞いをするので、上3つの現象がおきてしまう。

情報収集

日本語で探したところ以下の2つの記事がヒットした。なぜもっとないのだろう。同じ問題ぶつかっている人多いはずなのに。。。

https://kohei1116.hateblo.jp/entry/2020/02/16/aws-cognito
https://doroidpanic.com/aws-cognitoでemailを変更した場合、認証してなくても古いメ/

issueみつけた

またこちらのissueがこの問題を中心に扱っており、ひととおり情報が収集できるはず。

https://github.com/aws-amplify/amplify-js/issues/987

結論から言うと、Cognitoのサービス側の問題であり、解決策は提供されていない。上のissueとか3年前からずっと続いている。

またこちらに対するamazonのチームからのフィードバックコメントがこちら。

https://github.com/aws-amplify/amplify-js/issues/987#issuecomment-531025897

端的にいうと、問題は認識してるし、今後のTODOリストには載っているよとのこと。(こちらとしては解決策求めてたのに、ちょっとまってねって。。)

issueで有志から提案された解決策は2つ

  1. 最初に共有した記事の人が行っているように、独自に検証プロセスを設ける方法。
  2. cognitoのカスタムトリガーを利用してちょっとトリッキーだけど、変更を塗りつぶしてしまう方法。参照

1のほうは最初に共有した記事がまんま同じことをやっている。

2つめのほうをここで共有したい。ただし先にいうと2つめにはユーザーが実際の検証なしに検証させてしまうセキュリティホールがあるよとのこと(ここ深く読んでいなので、だれか解説お願いします。)

セキュリティホールあるかもよ。

https://github.com/aws-amplify/amplify-js/issues/987#issuecomment-561731822

2つめのほうの解説

前提知識

cognitoにはtriggerという機能があり、それを使って例えばサインアップ処理前後に処理を挟めたりする。フック的なもの。

その中の一つに、カスタムメッセージトリガーがある。こいつはもともとメールアドレス認証とかのメール本文(やSMS)をカスタマイズするために提供されている。これを利用して最初に説明した動作を握りつぶしてしまおうということ。

step1: メールアドレスを更新する

フロントにてメールアドレスを変更する部分の処理。通常であればemailだけを指定するが、ここでcustome:validated_email に古い方のメールアドレスを指定する。

const result = await Auth.updateUserAttributes(cognitoUser, {
  email: NEW_EMAIL,
  'custom:validated_email': CURRENTLY_VALIDATED_OLD_EMAIL,
});

次にLambdaを作成してカスタムメッセージイベントをハンドリングする。カスタムメッセージtriggerに関する詳細はこちら

最初に述べたようにupdateUserAttributesが実行されている時点でemailが変わってしまい、validatedがfalseになる。そこでそれらを先程のcustome:validated_emailを利用してもとに戻す。

exports.handler = (event, context, callback) => {
    // ユーザープールidが一致するか確認
    if(event.userPoolId === "theSpecialUserPool") {
        // Identify why was this function invoked
	// ここがUpdateUserAttributeになることに注意。
        if(event.triggerSource === "CustomMessage_UpdateUserAttribute") {
	  const validated_email = event.request.userAttributes['custom:validated_email'];
	  const params: AdminUpdateUserAttributesRequest = {
	    // cognitoによって勝手に変えられていた2つをもとにもどす。
	    UserAttributes: [
	      {
		Name: 'email_verified',
		Value: 'true',
	      },
	      {
		Name: 'email',
		Value: validated_email,
	      },
	    ],
	    UserPoolId: event.userPoolId,
	    Username: event.userName,
	  };
	  const result = await cognitoIdServiceProvider.adminUpdateUserAttributes(params).promise();
	  // メールアドレス検証後にadminUpdateUserAttributeが実行される。このときにこのラムダが事項されてはまずいので、それを回避する。
	  if (validated_email === event.request.userAttributes.email) {
	    throw new Error('failed to prevent sending unnecessary verification code');
	  }
        }
    }

    callback(null, event);
};

step2: 新しいメールアドレスを検証する

検証コードを入力するやつ。このとき成功すれば、もう一度ユーザー情報をupdateする。なぜなら、step1で古いメールになっているので更新しなければいけないから。このときemailと'custom:validated_email'の両方に新しいメールアドレスをいれる。

const result = await Auth.verifyUserAttributeSubmit(cognitoUser, 'email', code);
if (result === 'SUCCESS') {
  const result2 = await Auth.updateUserAttributes(cognitoUser, {
    email: NEW_EMAIL, // ここと
    'custom:validated_email': NEW_EMAIL, // ここに新しいメールアドレスをいれる。 
  });
}

このとき再びstep1のラムダが起動してしまうが(updateUserAttributesで発火するようにしているから)、以下のコードの部分おかげで、特に何も起きない。

  if (validated_email === event.request.userAttributes.email) {
    throw new Error('failed to prevent sending unnecessary verification code');
  }

step3

cognitoのユーザープールの設定でcustom: validated_email を加える。triggerの設定でカスタムメッセージにStep1で作成したLambdaを指定する。

以上でOK!他にdynamodb等を利用することもないし、一番シンプルかも。

さいご

なんでずっと放置されてるんだろう。はやく根本的に解決してほしい。

Discussion

有益な情報ありがとうございます。
lambdaコード中のcognitoIdServiceProvider

const AWS = require('aws-sdk');
const cognitoIdServiceProvider = new AWS.CognitoIdentityServiceProvider();

などで生成する感じでしょうか?

[追記]
上記の実装方法で解決致しました。失礼しました。

ログインするとコメントできます