Zenn
🔑

【後編】AWS初心者向け:React + Amazon Cognitoで認証機能を作るチュートリアルを深掘りする

2025/03/27に公開
1

はじめに

この記事では、AWS公式のAmazon CognitoのQuick Setup ガイド(2025/03時点)をベースにして、ReactアプリケーションにAmazon Cognitoを使用した認証機能を実装する方法を解説していきます。

Quick Setup ガイドで前提条件として求められる認証の基本概念の補足を行いながら、トークンの取り扱いに関してより実践的に深掘りしていくことを目指します。

この記事は前後編の後編にあたります。
前編はこちら

目次

【前編】

  1. Amazon Cognitoの設定

    • ユーザープールの作成
    • アプリケーションクライアントの設定(Quick Setup ガイド①)
    • ユーザーの作成
  2. Reactでの認証機能実装

    • プロジェクトのセットアップ
    • シングルサインオン(SSO)、OIDCとは
    • react-oidc-contextの導入(Quick Setup ガイド②)
    • 認証状態の管理
    • ログイン/ログアウトの実装(Quick Setup ガイド③、④)

【後編】
3. トークンの取得と管理

  • 発行されるトークンについて
  • トークンの有効期限管理
  1. 自動ログイン実装

    • react-oidc-contextライブラリにおける自動ログイン実装
  2. トークンを用いて他のサービスに認証(APIGatewayを例とする)

    • Cognitoオーソライザーの設定
    • APIを再デプロイ
    • オーソライザーテスト
    • トークンを使用したAPI認証

3. トークンの取得と管理

3-1. 発行されるトークンについて

Amazon Cognitoは認証成功時に3種類のトークンを発行します。これらのトークンはJWT(JSON Web Token)形式で発行されます。

各トークンの役割は以下の通りです。

  1. IDトークン

    • ユーザーのプロフィール情報(メールアドレス、名前など)を含む
    • ユーザーの認証にも用いる
    • フロントエンド側でユーザー情報の表示に使用
  2. アクセストークン

    • APIリクエスト時の認証に使用
    • ユーザーの権限情報とスコープ(許可された操作範囲)を含む
  3. リフレッシュトークン

    • 他の2つのトークンを更新するための鍵のような役割
    • IDトークンとアクセストークンの有効期限が切れた時に新しいトークンを取得
// トークンの確認例
const auth = useAuth();
console.log(auth.user?.id_token);      // IDトークン
console.log(auth.user?.access_token);   // アクセストークン
console.log(auth.user?.refresh_token);  // リフレッシュトークン

参考:

3-2. トークンの有効期限管理

アプリの使用中にトークンの有効期限が切れるとユーザー視点では急にサービスが使えなくなるということが起こりえます。
トークンの有効期限を適切に調節したり更新することで、セキュリティと利便性を高めることが重要です。

トークンの有効期限設定

  • アプリケーションクライアントの設定から各トークンの有効期限を変更可能
  • IDトークン・アクセストークン:デフォルトで1時間
  • リフレッシュトークン:デフォルトで30日(最大10年まで設定可能)

有効期限の監視と自動更新

トークンの有効期限を監視し自動更新を行うサンプルコードです。

src/hooks/useTokenExpiry.js
import { useAuth } from "react-oidc-context";
import { useEffect } from "react";

export const useTokenExpiry = () => {
  const auth = useAuth();

  useEffect(() => {
    if (auth.isAuthenticated) {
      // トークンの有効期限をチェック
      const tokenExpirationTime = auth.user?.expires_at * 1000; // ミリ秒に変換
      const currentTime = Date.now();
      const timeUntilExpiry = tokenExpirationTime - currentTime;

      // 有効期限が近づいたら警告(5分前)
      if (timeUntilExpiry < 300000) {
        console.warn("トークンの有効期限が近づいています");
      }

      // 有効期限切れの場合は自動更新を試行
      if (timeUntilExpiry <= 0) {
        auth.signinSilent().catch(() => {
          // サイレント更新に失敗した場合はログイン画面へリダイレクト
          auth.signinRedirect();
        });
      }
    }
  }, [auth.isAuthenticated]);
};

4. 自動ログイン実装

4-1. セッション永続化の必要性

react-oidc-contextではデフォルトでトークン情報をメモリ上で管理します。
そのため、ブラウザのタブを閉じるとトークン情報が消失し、ログイン状態が失われてしまいます。
これを防ぐために、LocalStorageを使用してセッション情報を永続化する必要があります。
ただし、LocalStorageにトークンを保存することはセキュリティリスクも生じるため、保存時に暗号化したり有効期限を短くするなど、設定に注意が必要です。

4-2. 自動ログインの実装

Reactにおける自動ログインの実装には以下の2つのステップが必要です。

  1. LocalStorageにセッション情報を保存
  2. 自動ログインロジックの実装

LocalStorageにセッション情報を保存

src/main.jsx
import { WebStorageStateStore } from "oidc-client-ts";

const cognitoAuthConfig = {
  authority: "https://cognito-idp.{リージョン}.amazonaws.com/{ユーザープールID}",
  client_id: "{クライアントID}",
  redirect_uri: "http://localhost:5173",
  response_type: "code",
  scope: "email openid phone",
+  // LocalStorageにセッション情報を保存
+  userStore: new WebStorageStateStore({ store: window.localStorage }),
};

自動ログインロジックの実装

import React, { useEffect } from 'react';
import { useAuth, hasAuthParams } from "react-oidc-context";

function Login() {
  const auth = useAuth();
  // ログイン試行フラグ:無限ループを防ぐために使用
  const [hasTriedSignin, setHasTriedSignin] = React.useState(false);
  // hasClickedLoginの初期値をfalseに設定(localStorage に値がない場合は false)
  const hasClickedLogin = localStorage.getItem('hasClickedLogin') !== null 
    ? localStorage.getItem('hasClickedLogin') === 'true' 
    : false;

  // 自動ログイン処理
  useEffect(() => {
    // 以下の全ての条件が満たされた場合にのみサインインを試行
    if (!hasAuthParams() &&           // 1. 認証パラメータがURLに存在しない
        !auth.isAuthenticated &&      // 2. まだ認証されていない
        !auth.activeNavigator &&      // 3. 認証プロセスが進行中でない
        !auth.isLoading &&            // 4. ローディング中でない
        !hasTriedSignin &&           // 5. まだサインインを試していない
        hasClickedLogin              // 6. 以前にログインボタンをクリックしている
    ) {
      auth.signinRedirect();        // 認証プロバイダーへリダイレクト
      setHasTriedSignin(true);      // ログイン試行フラグを立てる
    }
  }, [auth, hasTriedSignin, hasClickedLogin]);

  // ログインボタンクリック時の処理
  const handleLogin = () => {
    localStorage.setItem('hasClickedLogin', 'true');  // ログイン許可状態を保存
    auth.signinRedirect();
  };
  
  // ログアウト処理
  const handleLogout = () => {
    localStorage.setItem('hasClickedLogin', 'false');  // ログイン不許可状態に設定
    auth.removeUser(); // ユーザー情報を削除
    navigate('/'); // ホームページにリダイレクト
  };

  return (
    <div>
      <button onClick={handleLogin}>ログイン</button>
      <button onClick={handleLogout}>ログアウト</button> {/* ログアウトボタンを追加 */}
    </div>
  );
}

この実装により、以下のような動作が実現できます。

  • ユーザーが初めてアプリにアクセスした時は自動ログインを行わない
  • ユーザーが明示的にログインボタンをクリックした後は、タブを閉じて再度開いた場合に自動ログインを試行
  • セッション情報がLocalStorageに保存されるため、ブラウザを閉じても認証状態を維持

参考:react-oidc-context 自動ログイン実装

5. トークンを用いて他のサービスに認証

トークンを用いることで、ユーザーは他のサービスに認証を通すことができるようになります。
ここでは、例としてAPI Gatewayへの認証を行う手順を解説します。

前提条件

  • API GatewayのAPIをすでに持っていること

5-1. Cognitoオーソライザーの設定

Cognitoオーソライザーを使用することで、API Gatewayでのリクエストを認可することができます。以下の手順で設定を行います。

  1. AWSマネジメントコンソールにログインし、API Gatewayを選択
  2. 対象のAPIを選択し、メニューバーの「オーソライザー」をクリック
  3. 「オーソライザーの作成」を選択し、以下の設定を行います。
    • オーソライザー名: 任意の名前を設定
    • オーソライザーのタイプ: Cognitoを選択
    • ユーザープール: 作成したCognitoユーザープールを選択
    • トークンソース: Authorization ヘッダーを指定

5-2. APIを再デプロイ

  1. 対象のAPIを選択し、メニューバーの「リソース」をクリック
  2. GETやPOSTなど任意のメソッドを選択し、「編集」を選択して以下の設定を行います。
    • 認可: 作成したオーソライザーを設定
    • 認可スコープ: なし
    • リクエストバリデーター: クエリ文字列パラメータまたはヘッダーを検証を選択(ヘッダーを含めていれば良いです)
    • オペレーション名: なし
  3. メニューバーの「ステージ」から対象のAPIを再デプロイ

これで、デプロイしたAPIからCognitoによる認証を行えるようになります。

5-3. オーソライザーテスト

オーソライザーが正しく機能しているかをテストするために、以下の手順を実行します。

  1. 対象のAPIを選択し、メニューバーの「オーソライザー」をクリック
  2. 作成したオーソライザーを選択
  3. IDトークンを使用して「オーソライザーをテスト」を選択してリクエストを行い、200 OKのレスポンスが返ってくることを確認
  4. 無効なトークンやトークンなしでリクエストを行い、401 Unauthorizedのレスポンスが返ってくることを確認
// トークンの確認例
const auth = useAuth();
console.log(auth.user?.id_token);      // IDトークン
console.log(auth.user?.access_token);   // アクセストークン
console.log(auth.user?.refresh_token);  // リフレッシュトークン

5-4. トークンを使用したAPI認証

APIを呼び出す際には、Cognitoから取得したアクセストークンを使用します。以下は、axiosを使用して認証されたリクエストを送信するサンプルコードです。

import { useState } from "react";
import axios from "axios";

// トークンを取得する関数
const getAccessToken = () => {
  const storageKey = 'oidc.user:https://cognito-idp.ap-southeast-2.amazonaws.com/ap-southeast-2_qsJXNKTRH:11rtumj9menqpcdg9lbeghpnt8';
  try {
    const userJson = localStorage.getItem(storageKey);
    if (userJson) {
      const user = JSON.parse(userJson);
      if (user.expires_at) {
        const expirationTime = user.expires_at * 1000; // Unix timestamp をミリ秒に変換
        const currentTime = new Date().getTime();
        
        // トークンの有効期限が切れているかチェック
        if (currentTime >= expirationTime) {
          console.warn('トークンの有効期限が切れています。');
          return null;
        }

        return user.id_token; // 有効なトークンを返す
      }
    }
  } catch (error) {
    console.error('トークン取得エラー:', error);
  }
  return null;
};

// サンプルAPIを呼び出すコンポーネント
export const SampleAPICaller = () => {
  const [responseData, setResponseData] = useState(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleAPICall = async () => {
    setIsSubmitting(true);
    const accessToken = getAccessToken(); // トークンを取得

    if (!accessToken) {
      console.error("アクセストークンが取得できませんでした。");
      setIsSubmitting(false);
      return;
    }

    try {
      const response = await axios({
        url: "https://your-api-endpoint.amazonaws.com/sample", // サンプルAPIのエンドポイント
        method: 'get',
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`, // アクセストークンを含める
        },
      });

      if (response.status === 200) {
        setResponseData(response.data); // 取得したデータを保存
        console.log("取得したデータ:", response.data);
      }
    } catch (error) {
      console.error('API呼び出しエラー:', error.response || error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div>
      <h1>サンプルAPI呼び出し</h1>
      <button onClick={handleAPICall} disabled={isSubmitting}>
        APIを呼び出す
      </button>

      {responseData && <div>取得したデータ: {JSON.stringify(responseData)}</div>}
    </div>
  );
};

まとめ

この記事では、React + Amazon Cognitoを使用した認証機能の実装方法を学びました。

参考資料

お疲れ様でした!

この記事は以上になります。お疲れさまでした。

1

Discussion

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