[AWSxAmplifyxCognito] Amplifyでグローバルサインアウトがグローバルサインアウトしてくれない問題の応急処置


はじめに

こんにちは!
認証機能を実装する場合、別のデバイスでサインアウトしたらユーザーがログインしている他の全てのデバイスでもログアウトして欲しいときがありますよね?

Amplifyではそんな要望に応えるために、singOut関数にglobalオプションがあり、signOut({global:true})のようにするとサインアウトした際に、ユーザーが持つ全トークンが無効化され、全てのデバイスからサインアウトできます。
以下ドキュメントから抜粋

と、いうのがドキュメントには書かれており、実際ユーザーが認証後に取得する3種類のトークンのうち、アクセストークンとリフレッシュトークンは即座に無効になるのですが、
実は、signOut({global:true})をしてもIDトークンは無効にならないんです!
加えて、セッションを確認するための関数たち(currentSession,userSession)はCognitoにアクセストークンやリフレッシュトークンが有効なのか問い合わせにいったりはせず、ローカルストレージにあるIdトークンの有効期限を確認するだけなんです。

つまり、currentSession,userSession関数では、1つのデバイスでグローバルサインアウトが行われていても、他のデバイスではIdトークンが有効期限切れにならない限り、ユーザーは"セッション切れ"とは判断されないんです。

対応策: 実行時にアクセストークンを使う関数を利用して、セッション切れを判断する

currentSession,userSession関数では、アクセストークン、リフレッシュトークンが有効かどうか見てくれない。
なら、それらが有効かどうかを問い合わせる関数を使えばいいのですが、その確認のためだけの関数がAmplifyリファレンスに見当たらないんです。

なので、実行時にアクセストークンを使う関数を利用して、その有効・無効を判断します。
例えば、currentUserInfo関数がそれにあたります。

この関数は,

  • サインイン状態のとき、以下のようなusername,attributes(cognitoで設定したユーザーの属性)を含むオブジェクトが返ります。
{
    "UserAttributes": [
        {
            "Name": "sub",
            "Value": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        },
        {
            "Name": "email_verified",
            "Value": "true"
        },
        {
            "Name": "email",
            "Value": "[email protected]"
        }
    ],
    "Username": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
}
  • そのデバイスでサインアウトして、デバイスにトークンがないときはnull
  • 別のデバイスでサインアウトして、そのデバイスに各種トークンはあるが、アクセストークンが無効なときは、空オブジェクト{}が返ります

したがって、返り値が null または 空オブジェクト のときユーザーはサインアウト状態であるといえます。

以下はサインイン画面での実装例です。

サインイン画面側の実装例

サインインしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト)の否定である nullでない かつ 空オブジェクトでない でサインイン状態かどうかを見ています。(空オブジェクトでない はプロパティがundefinedかどうかで見ています。)


const getCurrentUserInfo = async () => {
    const currentUserInfo = await Auth.currentUserInfo();
    return currentUserInfo;
}
getCurrentUserInfo()
    .then(currentUserInfo =>{
        //以下の場合はサインアウト状態
        //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき
        //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき
        if(currentUserInfo !== null && currentUserInfo.username !== undefined){
            //サインインしているときにしたい処理を書く。ログイン画面に飛ばすとか。
        }
    })
    .catch(err=> {
        console.log(err)
    })

サインイン後の画面の実装例

サインアウトしてる時のみ、処理を行いたいので
サインアウト状態(null or 空オブジェクト)でサインアウト状態かどうかを見ています。

const getCurrentUserInfo = async () => {
    const currentUserInfo = await Auth.currentUserInfo();
    return currentUserInfo
}
getCurrentUserInfo()
    .then(currentUserInfo =>{
        // 以下の場合はサインアウト状態なのでログイン画面に飛ばす。
        //1.currentUserInfo === null:そのデバイスでサインアウトして、デバイスにトークンがないとき
        //2.currentUserInfo.username(などユーザーに関するプロパティ) === undefined: 別のデバイスでサインアウトして、そのデバイスにトークンはあるが、アクセストークンが無効なとき
        if(currentUserInfo === null || currentUserInfo.username === undefined){
            //ログイン画面に飛ばす処理
        }
    })
    .catch(err=> {
        console.log(err)
        //currentUserInfoが取得できないときはログイン画面に飛ばす。
        //ログイン画面に飛ばす処理
    })

以上です。
上記の処理をページの描画ごとに行えば、
Idトークンが有効であっても、
そのデバイスでサインアウトしている場合だけでなく、別のデバイスでサインアウトしているかどうかも判定ができます。

自分の方法が唯一解ではないと思うので、別案・改善案あればぜひ共有していただければ!

終わりに

公式の見解

実は、signOut({global:true})がドキュメント通りの挙動でない問題は現在openなissueとしてあがっています。
cognito.user.signOut() does not invalidate tokens

このissueは2019年6月にopenされて現在もまだcloseされていません。このissue以外にも同様の件に関するissueは何件か見られました。

ユーザーの1人がコメントで個人的にAWSに問い合わせした際の返事を転記していて、それを信じるなら、AWS側はこの問題を認知していて現在取り組み中だそうです。。

以下のどちらかがいつか実装されるといいなと思います。できれば前者。

  • グローバルサインアウトでidトークンが即時無効になり、かつ session関数がローカルストレージのトークン有効期限ではなくcognitoに有効かどうかを確認しに行く
  • アクセストークンが有効かどうかをCognitoに問い合わせる関数が生える