【AWS】CognitoでGoogleソーシャルログイン。Amplifyと連携してVue.jsアプリケーションに組み込む


前回、Qiita初投稿させて頂いた、個人開発のAWSサーバーレスWEBサイト「ボケさせて(BOKESASETE)」ですが、

前回記事
【個人開発】AWSサーバーレスアーキテクチャで機械学習とボケ。フロントエンドはVue.jsで遊ぶ

おかげさまで、徐々にではありますが、ご登録頂けるユーザーも増えつつあります。

しかしながら、現状のフローではメールアドレス認証による会員登録しかできない状態となっていて、モダンなWEBサイトとは言えません。
せめて、ソーシャルログインぐらいは欲しいところです。

そこで今回、AWS Cognito × Vue.jsのWEBサイトにGoogleでログインするソーシャルログイン機能を追加しました、という話。

はじめに


写真で一言。ボケてみるよりボケさせて - BOKESASETE
https://bokesasete.me/

ボケさせて(BOKESASETE)では、会員情報の管理にAWS Cognitoを利用しています。

今回、Cognitoのフェデレーションという仕組みを利用し、ユーザープールとGoogleアカウントが連携できるよう機能改修しました。

公式
サードパーティー経由のユーザープールへのサインインの追加

やり方

Cognito側の設定

基本的には、以下記事にて詳細手順をまとめて頂いておりますので、ご参照ください。

参考
AWS CognitoにGoogleとYahooとLINEアカウントを連携させる

ただ、上記例ですと「ログインエンドポイント」を利用していますので、ボケさせて(BOKESASETE)のように、既にログイン画面をアプリケーション側で用意してしまっている場合、使い勝手が悪いです。

なので、次のSTEPでご紹介する「認証エンドポイント」を利用します。

認証ボタン設置

Cognitoの様々なエンドポイントについては、以下記事でかなり詳しくおまとめ頂いています。

参考
AWS Cognitoのエンドポイントを使いこなす

筆者の場合、「認証エンドポイント(許可されているOAuthフロー:Authorization Code Grant)」を利用し、既存のログイン画面に「Googleでログイン」ボタンを設置しました。

以下、設置例です(Vuetifyを利用してます)

Signin.vue

  ..略

  <a :href="COGNITO_BASE_URL+'/oauth2/authorize?identity_provider=Google&response_type=code&client_id='+USER_POOL_WEB_CLIENT_ID+'&redirect_uri='+COGNITO_REDIRECT_URL">
    <v-btn round large color="red darken-2" dark>
      <v-icon class="pa-2" small>fab fa-google</v-icon>
      Googleでログイン
    </v-btn>
  </a>

  ..略

※「COGNITO_BASE_URL」「USER_POOL_WEB_CLIENT_ID」「COGNITO_REDIRECT_URL」のところは環境に合わせて設定してください

上記「redirect_uri」で指定した戻り先URLに対して、認証を終えると「認可コード」が渡ってくる仕様です。

https://example.com/redirect?code=hogehogehugahuga
こんな感じでリダイレクトされて返ってきます。

さて、ここまでは参考記事のおかげで、割とすんなり実装できたのですが、ここからVue.jsで作ったアプリケーション(認証にはAmplifyを利用)に対して、どのようにログイン認証情報を渡せばよいのか、というところで行き詰まりました。

結論から申し上げると、今回は力技で乗り切りました。

なので、もっとスマートなやり方があるはずですので、どなたかもしご存知でしたらこっそりと教えてください。

やったこと

まず、Amplifyの認証情報を解析しました。

すると、localStorage内にトークン類(accessToken,idToken,refreshToken)が以下のようなルールで格納されていることが分かりました。

'CognitoIdentityServiceProvider.' + クライアントID + '.' + ユーザーID + '.accessToken'
'CognitoIdentityServiceProvider.' + クライアントID + '.' + ユーザーID + '.idToken'
'CognitoIdentityServiceProvider.' + クライアントID + '.' + ユーザーID + '.refreshToken'

Amplifyでは、ここに格納された認証情報を参照してログイン判定/処理を行っているに違いない

そのように考え、各エンドポイントを駆使して、自分でlocalStorageを書き換えて対応しました。

以下、コード抜粋です。

Redirect.vue
  // ..略

  // googleログインの場合、localstorageに認証情報を補完
  let code = this.$route.query.code
  if (code) {
    let params = new URLSearchParams();
    params.append('grant_type', 'authorization_code')
    params.append('redirect_uri', COGNITO_REDIRECT_URL)
    params.append('code', code)
    params.append('client_id', USER_POOL_WEB_CLIENT_ID)

    axios.post(COGNITO_BASE_URL + '/oauth2/token', params, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      })
      .then(token => {

        let bearer = 'Bearer ' + token.data.access_token
        axios.post(COGNITO_BASE_URL + '/oauth2/userInfo', {}, {
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
              'Authorization': bearer
            }
          })
          .then(userInfo => {

            // localStorageを書き換え
            localStorage.setItem('CognitoIdentityServiceProvider.' + USER_POOL_WEB_CLIENT_ID + '.' + userInfo.data.sub + '.accessToken', token.data.access_token)
            localStorage.setItem('CognitoIdentityServiceProvider.' + USER_POOL_WEB_CLIENT_ID + '.' + userInfo.data.sub + '.idToken', token.data.id_token)
            localStorage.setItem('CognitoIdentityServiceProvider.' + USER_POOL_WEB_CLIENT_ID + '.' + userInfo.data.sub + '.refreshToken', token.data.refresh_token)
            localStorage.setItem('CognitoIdentityServiceProvider.' + USER_POOL_WEB_CLIENT_ID + '.' + userInfo.data.sub + '.clockDrift', 0)
            localStorage.setItem('CognitoIdentityServiceProvider.' + USER_POOL_WEB_CLIENT_ID + '.LastAuthUser', userInfo.data.sub)

            // 正常処理
          })
          .catch(error => {
            // error 処理
          })
      })
      .catch(error => {
        // error 処理
      })
  }

  // ..略

以下のような処理の流れ。

  1. リダイレクト時に渡ってきた「認可コード」を利用し、「トークンエンドポイント」からトークン情報を取得
  2. 「手順:1」で取得した「アクセストークン」を利用し、ユーザープールの情報(ユーザーIDなど)を「USERINFOエンドポイント」から取得
  3. トークン情報およびユーザー情報をlocalStorageに格納

上記手順で、AWS Cognito × Vue.jsのWEBサイトにGoogleでログインするソーシャルログイン機能を実装することができました。

現在、特に問題なく動作しています。

ハマったこと

基本的には上記手順にて、問題なくログイン連携できるのですが、ハマった部分もありましたのでご紹介します。

認証成功してもユーザーの属性情報が取得できない

認証自体は成功したにもかかわらず、Cognito側からユーザーのニックネームやその他属性情報が取得できない状況に陥りました。

結論、これは凡ミスで「認証エンドポイント」に「&scope=openid%20email」というパラメタが付与されたままだった(コピペしたまま残っていた)ことが原因でした。

「Googleでログイン」ボタンのパラメタからscope指定を外すことで、Cognito側の設定が効いて、ユーザーのニックネームや属性情報が取得できるようになりました。
(もちろんCognitoのコンソール側で、以下のように「許可されているOAuthスコープ」を適切に設定しておく必要があります)

ちょいちょいログインが切れる

これはそもそも認証エンドポイント許可されているOAuthフローImplicit Grantを利用していることが原因でした。

Implicit Grantを利用した認証の場合、リダイレクト先に直接トークン類が渡ってきますので、Authorization Code Grantに比べて、一手間省けるため、当初この方式を採用していました。

しかしながらImplicit Grantの場合、「refreshToken」が付与されない仕様のため、1時間でアクセストークンの有効期限が切れ、結果的にログイン状態が頻繁に解除されてしまう現象に陥りました。

そこでAuthorization Code Grantによる認証フローに変更し、先に記載したような流れでトークン取得を行うようプログラム調整を行ったところ、期待した動作をする(ログイン情報が常に維持される)ようになりました。

終わりに

どなたかの役に立てば何よりです。