ReactにCognitoでサインイン機能をつける

Cognitoユーザープールを使用して、Reactアプリにサインイン機能を実装します。 今回はAmplifyを使用することで、認証・認可に関する様々なフローの実装を省略します。
2021.11.09

Cognitoで認証認可を実装する方法の整理

Cognitoを使用して認証認可をフロントエンドで実装する方法はいくつかあります。 自分はCognitoを使用するなら以下の3つが思いつきます。

  • AmplifyのAuthモジュールの利用して認証画面を自作
  • フロントエンドフレームワーク用のコンポーネントを使用する
  • CognitoユーザープールのHostedUIを利用

上の方が低レベルのAPIが利用でき、カスタマイズなどはしやすいです。 ただ、特別なフローが不要であったり、デザインに関して細かい要求がない場合は下のものを使うのが楽だと思います。 利用できるコンポーネントとしてはReactやAngular、Vueがあります。

今回は「CognitoユーザープールのHostedUIを利用」で進めていきます。

準備

準備するのは以下の二つです。

  • Reactアプリケーション
  • Cognitoユーザープール

Reactの準備

今回はcreate-react-appを利用してアプリケーションを作成します。 その後、amplifyパッケージをインストールします。

アプリケーションの作成

$ npx create-react-app amplify --template typescript
$ cd amplify
$ npm install --save aws-amplify

また、今回はローカルの開発でもHTTPSを使用する必要があります。 そのため、npmのpackage.jsonscriptsフィールドを以下のように設定して、ローカルでサーバーを立ててください。

package.json

  "scripts": {
    "start": "HTTPS=true react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

Congitoユーザープールの準備

今回はユーザープールは以下の手順に従って作成します。 簡略化のために認証はGoogleのソーシャルサインインのみで行います。

ただ一点、注意点があります。 アプリクライアントを作成する際にクライアントシークレットを生成しない ようにしてください。 作成時に以下のチェックボックスを外してください。 これはOAuthの認証フローである「Authorization code grant」を使用するためです。

アプリクライアントの設定は以下のようになります。

実装

準備が終わったのでReactに認証機能を実装していきます。 最終的なコードは以下のようになります。

App.tsx

import { useEffect, useState } from 'react';
import Amplify, { Auth, Hub } from 'aws-amplify';

Amplify.configure({
  Auth: {
    region: 'ap-northeast-1',
    userPoolId: 'ap-northeast-1_XXXXXXXXXX',
    userPoolWebClientId: 'XXXXXXXXXXXXXX',
    oauth: {
      domain: 'XXXXXXXXXXXXXX.auth.ap-northeast-1.amazoncognito.com',
      scope: ['openid'],
      redirectSignIn: 'https://localhost:3000/',
      redirectSignOut: 'https://localhost:3000/',
      responseType: 'code'
    }
  }
})

const Example = () => {
  const [user, setUser] = useState<any | null>(null);

  useEffect(() => {
    Hub.listen('auth', ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
        case 'cognitoHostedUI':
          getUser().then(userData => setUser(userData));
          break;
        case 'signOut':
          setUser(null);
          break;
        case 'signIn_failure':
        case 'cognitoHostedUI_failure':
          console.log('Sign in failure', data);
          break;
      }
    });

    getUser().then(userData => setUser(userData));
  }, []);

  const getUser = async () => {
    try {
      const userData = await Auth.currentAuthenticatedUser();
      // デバッグ用
      Auth.currentSession().then((data) => {
        console.log(`token: ${data.getIdToken().getJwtToken()}`);
      });
      console.log(userData);
      return userData;
    } catch (e) {
      return console.log('Not signed in');
    }
  }

  return user ? (
    <div>
      <p>サインイン済み</p>
      <p>ユーザー名: {user.username}</p>
      <button onClick={() => Auth.signOut()}>Sign Out</button>
    </div>
  ) : (
    <div>
      <p>
        サインインする
      </p>
      <button onClick={() => Auth.federatedSignIn()}>Sign In</button>
    </div>
  );
}

export default Example

動かしてみる

アプリケーションを動かしてみる

$ npm start

ブラウザでhttps://localhost:3000を開くと以下のような画面が出てきます。

サインインのボタンを押すとHostedUIにジャンプします。 Googleでの認証を行うと、localhostにリダイレクトされます。

するとログインが完了しています。

Amplifyの設定

Amplifyの設定

Amplify.configure({
  Auth: {
    region: 'ap-northeast-1',
    userPoolId: 'ap-northeast-1_XXXXXXXXXX',
    userPoolWebClientId: 'XXXXXXXXXXXXXX',
    oauth: {
      domain: 'XXXXXXXXXXXXXX.auth.ap-northeast-1.amazoncognito.com',
      scope: ['openid'],
      redirectSignIn: 'https://localhost:3000/',
      redirectSignOut: 'https://localhost:3000/',
      responseType: 'code'
    }
  }
})

AmplifyのAuthモジュールで利用する設定を書いています。 domainはユーザープール作成の際に設定したドメイン名です。 responseTypetokencodeが選べます。 codeの場合は「Authorization code grant」のフローに従うことになります。 tokenの場合は「Implict grant」となり、こちらは理由がなければ非推奨のようです。

イベントリスナーの設定

イベントリスナーの設定

const [user, setUser] = useState<any | null>(null);

useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
    switch (event) {
    case 'signIn':
    case 'cognitoHostedUI':
        getUser().then(userData => setUser(userData));
        break;
    case 'signOut':
        setUser(null);
        break;
    case 'signIn_failure':
    case 'cognitoHostedUI_failure':
        console.log('Sign in failure', data);
        break;
    }
});

getUser().then(userData => setUser(userData));
}, []);

const getUser = async () => {
try {
    const userData = await Auth.currentAuthenticatedUser();
    // 検証用
    Auth.currentSession().then((data) => {
    console.log(`token: ${data.getIdToken().getJwtToken()}`);
    });
    console.log(userData);
    return userData;
} catch (e) {
    return console.log('Not signed in');
}
}

Hubでイベントリスナーを登録します。 今回はauthイベントをハンドリングします。 サインインした際にはユーザーのデータをこのコンポーネントのステートに格納します。 サインアウトした際は削除します。 また、useEffectの最後で最初にこのコンポーネントをロードした際、以前の認証情報が残っていれば、認証済みにしています。

getUserはユーザーのデータを取得するためのヘルパー関数です。 非同期でユーザー情報が返ってくるのが注意点です。 サインインの方法によって、帰ってくるオブジェクトの型が大きく変わるので返り値の型はCognitoUser | Anyになっているようです。 検証用にトークンとユーザーデータを出力しています。

レンダリング

レンダリング

  return user ? (
    <div>
      <p>サインイン済み</p>
      <p>ユーザー名: {user.username}</p>
      <button onClick={() => Auth.signOut()}>Sign Out</button>
    </div>
  ) : (
    <div>
      <p>
        サインインする
      </p>
      <button onClick={() => Auth.federatedSignIn()}>Sign In</button>
    </div>
  );

Auth.federatedSignInは引数としてIdPを取ります(例: googleなど) しかし、何も渡さないと、CognitoユーザープールのHostedUIを利用します。 ここでは、usernullでない場合はサインイン済みとしています。

ユーザーデータとトークン

ここでのuserDataはCognitoUserクラスのインスタンスです。 この中には先ほど利用したuearnameやユーザープールの情報、セッション情報などが含まれています。

JWTトークンはAuth.currentSessionから取得できるセッション情報から入手できます。 このトークンをリクエストと一緒に送信することで、認可に使用することができます。 サーバーサイドではこのトークンの有効性を検証すれば良いです。

感想

Cognitoを使用して認証・認可を実装することができました。 個人的なハマりどころは「Authorization code grant」を使用する際は、クライアントシークレットを作成しないというところです。 この点に気をつければ簡単に実装できると思います。