既存のReactアプリに、Amazon Cognitoで認証を組み込んでみよう

記事タイトルとURLをコピーする

こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。

今回はReactを用いたSPA(Single Page Application)に、Amazon Cognitoで認証を組み込む方法を説明します。

本記事のターゲット

本記事は、SPAの開発を行うエンジニアを対象としています。
SPAのフロントエンドはReactで実装、バックエンドはAPI GatewayとAWS Lambdaを使用することを想定しているので、これらの技術に関する知識があることが望ましいです。

今回の構成

本記事では、以下の構成を想定しています。

構成図

  • フロントエンドはReactで、Vercelにデプロイ
  • バックエンドはAWS Lambdaで、Amazon API Gatewayを介してフロントエンドと通信
  • 認証はAmazon Cognitoを使用

Vercelを用いるのは、AWSリソース以外のサービスからでも、Amazon Cognitoを利用できることを示すためです。
フロントエンドとバックエンドは、同じアプリケーションクライアントを使用し、認証情報を共有します。

Amazon Cognitoのアプリケーションクライアント

docs.aws.amazon.com

Amazon Cognito(以降、Cognitoと略します)のアプリケーションクライアントは、ユーザープール内の設定です。
Cognitoのユーザープールが、認証と認可のためのユーザーディレクトリ(=ユーザー情報を保存する場所)として機能します。
そして、各アプリケーションは、アプリケーションクライアントを介してユーザープールにアクセスします。
同じアプリケーションクライアントを使用することで、フロントエンドとバックエンドは同じユーザープールを参照し、認証情報を共有することができます。

ReactアプリにAWS Amplify UIを使用して認証を組みこむ

まずは、フロントエンドのReactアプリに、AWS Amplify UIを使用して認証を組み込みます。

AWS Amplifyは、多数の機能を持つ開発プラットフォームです。
今回はこの中の一つ、Amplify UIを中心にしてReactアプリに認証を組み込みます。
最初からAmplify前提で作る方法もありますが、個人的に既存のReactアプリに後から組み込む方が必要な要素の把握には役立つと思いますので、後から組み込む方法を説明します。
手順自体はAWS公式のドキュメントに詳しく書かれていますので、こちらをベースにポイントを説明します。

docs.aws.amazon.com

サンプルコードを用いて説明していきます。
言語はTypeScriptを使用します。

設定ファイルの準備

まずは、Cognito用の設定ファイルを作成します。
ファイル名はsrc/config/aws-exports.tsとします。

// AWS Cognito configuration
const awsExports = {
  aws_project_region:
    process.env.REACT_APP_AWS_REGION || process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  aws_cognito_region:
    process.env.REACT_APP_AWS_REGION || process.env.NEXT_PUBLIC_AWS_REGION || 'ap-northeast-1',
  aws_user_pools_id:
    process.env.REACT_APP_COGNITO_USER_POOL_ID ||
    process.env.NEXT_PUBLIC_COGNITO_USER_POOL_ID ||
    'COGNITO_USER_POOL_ID',
  aws_user_pools_web_client_id:
    process.env.REACT_APP_COGNITO_CLIENT_ID ||
    process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID ||
    'COGNITO_USER_POOL_WEB_CLIENT_ID',
};

export default awsExports;

各種設定値は、環境変数から取得するようにしています。
環境変数名は、Vercelの命名規則に従っています。

Vercelの環境変数でこれらを設定することで、デプロイ時に自動的に値が埋め込まれます。
ユーザープールIDやクライアントIDは、AWSマネジメントコンソールから取得できます。

Cognitoのユーザープールやアプリケーションクライアントの設定については、こちらの記事を参考にしてください。

blog.serverworks.co.jp

ユーザープールの作成画面で、リターンURLの入力欄があります。
ここは一般的にフロントエンド側で用意しているログインページまたはトップページのURLを指定します。
不正なURLを入れると、認証に失敗します。
一旦デプロイしてURLを発行しておき、そのURLを入力しておくとよいと思います。

Cognitoのユーザープールの作成画面 - リターンURL

ユーザープール、アプリケーションクライアントを作成すると、それぞれIDが発行されます。

Cognitoのユーザープールの確認画面 - ユーザープールIDはここから

Cognitoのアプリケーションクライアントの確認画面 - クライアントIDはここから

この設定ファイルは以下のように使用します。

// Amplify設定をインポート
import { Amplify } from 'aws-amplify';
import awsExports from './config/aws-exports';

// Amplifyクライアントを設定
Amplify.configure({ ...awsExports });

Amplify UIのAuthenticatorコンポーネント

Amplify UIのAuthenticator<Authenticator>タグで挟んだ部分をログイン済みならその中身を、ログインしていなければログイン画面を表示します。
例えば、src/pages/Login.tsxに以下のように記述すれば、未ログインならログイン画面が表示され、ログイン後は認証が必要なページに転送させることができます。

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';

const AuthenticatedContent = () => {
  const { user, signOut } = useAuthenticator(context => [context.user]);
  const navigate = useNavigate();

  React.useEffect(() => {
    const timer = setTimeout(() => {
      navigate('/member');
    }, 2000);

    return () => clearTimeout(timer);
  }, [navigate]);

  return (
    <div>
      <h2>ログイン成功!</h2>
      <p>ユーザー: {user?.username}</p>
      <p>リダイレクトします...</p>
      <button
        onClick={signOut}
        style={{
          padding: '8px 16px',
          background: '#f44336',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
          marginTop: '10px',
        }}
      >
        ログアウト
      </button>
    </div>
  );
};

const Login: React.FC = () => {
  return (
    <div
      style={{
        maxWidth: '600px',
        margin: '0 auto',
        padding: '20px',
        boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
        borderRadius: '8px',
        marginTop: '50px',
      }}
    >
      <h1>ログイン / 新規登録</h1>
      <Authenticator
        initialState="signIn"
        services={{
          handleSignUp: async formData => {
            return {
              isSignUpComplete: false,
              nextStep: {
                signUpStep: 'CONFIRM_SIGN_UP',
                codeDeliveryDetails: {
                  deliveryMedium: 'EMAIL',
                  destination: formData.username,
                  attributeName: 'email',
                },
              },
            };
          },
        }}
      >
        {() => <AuthenticatedContent />}
      </Authenticator>
    </div>
  );
};

export default Login;

URLパスで認証の要否を制御する

<Authenticator>タグと、Reactのルーティングライブラリを組み合わせることで、URLパスに応じて認証の要否を制御できます。
例えば、React Routerを使用している場合、以下のようにルーティングを設定できます。

return (
<div className="app">
    <main>
        <Routes>
            <Route path="/about" element={<About />} />
            <Route
            path="/member/*"
            element={
                <Authenticator>
                <Member isLoggedIn={isLoggedIn} />
                </Authenticator>
            }
            />
        </Routes>
    </main>
</div>
);

これで/member以下のURLにアクセスしたときは認証が必要になり、/aboutでは認証なしでアクセスできるようになります。

認証済みかどうかの判定

認証済みかどうかは、fetchAuthSession関数を使用して判定できます。
この関数は、AmplifyのAuthモジュールからインポートでき、認証済みの場合はトークン文字が返却されます。

import { fetchAuthSession } from 'aws-amplify/auth';

const checkAuthState = async () => {
    try {
        const { tokens } = await fetchAuthSession();
        setIsLoggedIn(tokens !== undefined);
    } catch (error) {
        console.log('Not signed in');
        setIsLoggedIn(false);
    }
};

HTTPリクエストに認証情報を付与する

fetchAuthSessionで取得した認証情報をHTTPリクエストに付与すれば、認証済みのユーザーとしてAPIにアクセスできます。
BearerトークンをAuthorizationヘッダーに追加することで、認証情報を付与できます。

try {
    // Amplify v6 method usage
    const { tokens } = await fetchAuthSession();
    const token = tokens?.idToken?.toString();

    if (!token) {
        throw new Error('No token available');
    }

    console.info('Token obtained from Amplify:', token ? 'Valid token' : 'No token');

    const response = await fetch(apiConfig.endpoints.member, {
        headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
        },
    });

    console.info(response);
    
    if (!response.ok) {
        throw new Error(`API request failed with status ${response.status}`);
    }

    console.info('HTTP API endpoint succeeded');
} catch (error) {
    console.error('HTTP API endpoint failed:', error);
}

Amazon API GatewayにCognito認証を組み込む

今度は、バックエンドのAmazon API Gateway(以降、API Gatewayと略します)にCognito認証を組み込みます。
API Gatewayは、Cognitoをオーソライザーとして使用することで、認証を行うことができます。
オーソライザーを使用しなければそのAPIは認証が不要となります。

API Gatewayは、AWSマネジメントコンソールから設定できます。
ナビゲーションペインのAuthenticationをクリック。
オースライザーを作成してアタッチをクリック。

API GatewayのAuthentication - オーソライザーを作成してアタッチ

オーソライザーの作成画面が表示されるので、以下のように設定します。

  • オーソライザーのタイプ:JWT
  • 発行者URL:ユーザープールのトークン署名キーURLから、末尾の/.well-known/jwks.jsonを削除したものを入力
  • 対象者:(対象者を追加をクリックしてから)アプリケーションクライアントのクライアントIDを入力

オーソライザー作成画面 - JWTを選択して対象者を追加

オーソライザー作成画面 - 発行者URLと対象者を追加

Cognitoのユーザープールの画面 - トークン署名キーURLから末尾の/.well-known/jwks.jsonを削除して入力

ポイントは、発行者URLはユーザープールのトークン署名キーURLから、末尾の/.well-known/jwks.jsonを削除したものを入力すること。
対象者は一度追加をクリックしてから、アプリケーションクライアントのクライアントIDを入力することです。
また、クライアントIDはReactアプリのsrc/config/aws-exports.tsで設定したものと同じものを指定します。

オーソライザーを作成すると、JWT認証が付与されます。
後はデプロイして完成です。

API GatewayのAuthentication - JWT認証が付与されたらデプロイしておく

最後に

本記事は以上です。
認証が必要なAPI Gatewayのエンドポイントにアクセスすると、Cognitoで認証されたユーザーのみがアクセスできるようになります。
シンプルな内容ではありますが、皆様の参考になれば幸いです。

兼安 聡(執筆記事の一覧)

アプリケーションサービス部 DS3課所属
2025 Japan AWS Top Engineers (AI/ML Data Engineer)
2025 Japan AWS All Certifications Engineers
2025 AWS Community Builders
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。
X(旧Twitter)