9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】S3+CloudFrontで配信するReactアプリに、Cognitoログインを導入して認証付きAPIを実行する

Last updated at Posted at 2025-12-14

はじめに

この記事では、Reactで作成したアプリをAWS上で動かすための「基本構成」を、一つずつ組み立てながら解説していきます。
S3 + CloudFrontでのホスティングCognitoでのユーザー認証Lambda + API Gatewayを使ったAPI作成という、実践でも使える構成を取り上げます。

内容が盛りだくさんなので、「ホスティングだけ知りたい」「認証まわりだけ見たい」など、気になるところだけ拾い読みして参考にしていただければ幸いです。

この記事が役に立つ人

  • Reactで作成したアプリをAWSにデプロイして公開しようとしている人
  • Cognitoを利用してアプリにログイン機能を実装しようとしている人
  • アプリから認証付きのAPIを呼べるようにしたい人
  • とりあえず動く構成を作って、そこから自分のアプリへ発展させたい人

作成するもの

以下の構成のWebアプリケーションを作成します。

architecture_01.png

主な構成要素は以下の3つです。これらが連携することで、「アプリにログイン → アプリ内からAPI実行」という一連の流れが成り立つ構成になっています。

1. 認証対応のSPA (Single Page Application)

  • 技術スタック:React (TypeScript) / S3 / CloudFront
  • 役割:
    • 未ログインユーザーをCognitoログインページへ遷移させる
    • ログイン後の認可コードを受け取ってトークンを取得する
    • ログイン後のユーザー情報を保持し、IDトークンを使ってAPIを呼び出す

2. ユーザー認証基盤

  • 技術スタック:Cognito
  • 役割:
    • ユーザー名・メールアドレスなどの属性を管理する
    • ログイン処理・トークン発行を行う
    • API実行時のトークン検証を行い、認証済みユーザーのみがAPIを実行できるようにする

3. 認証付きAPI

  • 技術スタック:FastAPI / Lambda / API Gateway
  • 役割:
    • SPAからのAPIリクエストを受け取り、Authorizationヘッダー (Bearer ID Token) を検証する
    • 正しいトークンが付与されている場合のみ処理を実行する

使用しているツール・バージョン

本記事では以下のツール・バージョンで作業を行っています。

フロントエンド (React / Typescript)

  • Node.js:v22.17.1
  • npm:v10.9.2
  • TypeScript:v5.9.2
  • React:v19.1.1
  • Vite:v7.1.7
  • react-oidc-context:v3.3.0

API (FastAPI / Lambda)

  • Python:v3.12
  • FastAPI:v0.122
  • Mangum:v0.19

1. アプリをS3 + CloudFrontにデプロイ

まずは基盤となるReactアプリを用意して、それをS3とCloudFrontを使ってインターネットに公開できる状態にします。

architecture_02.png

Reactプロジェクトの作成

フロントエンド開発には Vite を使用します。(別のビルドツールでも問題ありません。)
セットアップは対話形式で進むので、以下のように選択して進めてください。

$ npm create vite@latest

◇  Project name:
│  react_auth_app
│
◇  Select a framework:
│  React
│
◇  Select a variant:
│  TypeScript
│
◇  Use rolldown-vite (Experimental)?:
│  No
│
◇  Install with npm and start now?
│  Yes

プロジェクトが作成できたら、ローカル開発サーバーを起動します。

$ npm run dev

ブラウザで http://localhost:5173/ にアクセスして、初期画面が表示されれば準備完了です。

vite+react.png

S3バケットの作成

次に、このReactアプリをホストするためのS3バケットを作成します。

s3_01.png

セキュリティのため、パブリックアクセス設定は「パブリックアクセスをすべてブロック」にします。

s3_02.png

アプリをS3にアップロード

アプリをビルドして、配信用の静的ファイルを出力します。

$ npm run build

dist/ フォルダ生成されたファイル一式を、そのままS3バケットへアップロードします。

s3_03.png

これで、アプリのホスティング準備が整いました。

CloudFrontディストリビューションの作成

続いて、S3にアップロードしたアプリをCloudFront経由で配信できるように設定します。

1. 基本設定
名前等を設定します。今回は独自ドメインは設定せず、そのまま進みます。

cloudfront_01.png

2. オリジンの設定
先ほど作成したS3バケットをオリジンに指定します。パスは /index.html とします。

cloudfront_02.png

  • バケットへのプライベートアクセスを許可
  • キャッシュ設定は今回は Caching Disabled(S3ソース更新時にキャッシュクリアが不要になるため)

cloudfront_03.png

cloudfront_04.png

3. セキュリティ設定
必要に応じてWAFを設定できます。(推奨)

cloudfront_05.png

4. 内容を確認して作成
作成後、自動で S3 バケットのポリシーが更新されるので確認してください。更新されていない場合は手動で設定してください。

S3_04.png

設定が反映され、https://[ディストリビューションドメイン].net にアクセスできればデプロイ完了です。

vite+react_2.png

以下のように Access Denied (403エラー) となる場合は、ディストリビューションの「エラーページ」タブで403のカスタムエラーを追加し、レスポンスページのパスに /index.html を設定してください。

vite+react_3.png

cloudfront_06.png

2. Cognitoログインページを作成

S3+CloudFrontのデプロイによりアプリの配信が可能となりましたが、このままではURLを知っていれば誰でもアプリにアクセスできてしまう状態になっています。そこで、Cognitoを利用したユーザ認証を実装します。
まずは、Cognito側の設定でアプリへのログインページを作成します。

architecture_03.png

Cognitoユーザープール・アプリケーションクライアントの作成

まずはユーザープールとアプリケーションクライアントを作成します。
アプリケーションタイプは「シングルページアプリケーショ (SPA) 」を選択します。

cognito_01.png

サインイン関連のオプション設定は各自の目的に応じて設定します。
今回はサインイン識別子にメールアドレスとユーザー名を選択、サインアップの必須属性にメールアドレスを設定しました。また、自分が作成したユーザーのみがサインインできるようにしたいので、自己登録は無効にします。

cognito_02.png

リターンURLには、さきほどアクセス確認をしたアプリのURL(https://[CloudFrontディストリビューションARN].net)を設定します。

cognito_03.png

作成ボタンを押すと、ユーザープールと、その中にアプリケーションクライアントが作成されます。それぞれ、わかりやすい名前に変更しておきます。

cognito_04.png

cognito_05.png

Cognitoユーザー作成

作成したユーザープールにユーザーを追加します。

cognito_06.png
cognito_07.png

ログイン確認

アプリケーションクライアントの概要画面にある「ログインページを表示」をクリックしてログイン画面を開きます。

cognito_08.png

ユーザー認証情報の入力と初回のパスワード変更を行い、ログインに成功すると、CloudFrontで配信しているアプリにリダイレクトされます。

app_01.png
app_02.png
app_03.png

以上でログインページの作成が完了しました。

3. アプリ内にユーザー認証機能を実装

ここからは、Reactアプリ側にCognitoの認証機能を組み込んでいきます。
実装する主な機能は以下の通りです。

  • 未ログインのユーザーをCognitoのログイン画面へ自動リダイレクト
  • ログイン後のユーザー情報(ユーザー名、メールアドレス、トークン)の取得と管理
  • Context APIを使ったアプリ全体への認証状態の共有
  • 開発環境では認証をスキップする仕組み(開発効率のため)

ディレクトリ構成

認証機能は以下のような構成で実装します。実際のプロジェクトに合わせて調整しても問題ありません。

src/
  ├── auth/                      # 認証関連のモジュール一式
  │   ├── authConfig.ts          # Cognitoの接続設定
  │   ├── authContext.ts         # 認証情報を共有するContext
  │   ├── AuthProvider.tsx       # 認証機能を管理し、Contextを提供するProvider
  │   ├── AuthGuard.tsx          # 認証保護コンポーネント
  │   ├── type.ts                # 型定義
  │   └── useAuth.ts             # 認証情報にアクセスするフック
  ├── App.tsx                    # メインアプリケーション
  └── main.tsx                   # エントリーポイント

各ファイルの役割

ファイル 役割 依存関係
type.ts ユーザー情報 / 認証状態の型定義 なし
authConfig.ts Cognitoの接続設定 なし
authContext.ts 認証状態を保持するContext type.ts
AuthProvider.tsx 認証状態の管理とContextの提供 authConfig.ts / authContext.ts / react-oidc-context
useAuth.ts Contextの認証情報へアクセスするカスタムフック authContext.ts
AuthGuard.tsx 認証が必要な画面を保護するガード useAuth.ts
App.tsx 認証機能を実際に使うコンポーネント AuthGuard.tsx / useAuth.ts
main.tsx AuthProviderでアプリをラップ AuthProvider.tsx / App.tsx

実装の流れ

認証周りは依存関係が強いため、以下の順番で実装するとスムーズです。

  1. 型定義 (type.ts)
  2. 設定ファイル (authConfig.ts)
  3. Context定義 (authContext.ts)
  4. Provider実装 (AuthProvider.tsx)
  5. カスタムフック (useAuth.ts)
  6. ガードコンポーネント (AuthGuard.tsx)
  7. 統合 (main.tsx / App.tsx)

ステップ1. 型定義の作成(type.ts)

最初に、アプリケーション全体で使用する認証関連の型を定義します。

type.ts
/** アプリケーション内で使用するユーザー情報の型定義 */
export interface User {
    username: string;       // ユーザー名
    email: string;          // メールアドレス
    idToken: string;        // IDトークン
    accessToken: string;    // アクセストークン
    refreshToken?: string;  // リフレッシュトークン
}

/** 認証状態を表す型定義 */
export interface AuthState {
    user: User | null;               // ログイン中のユーザー情報(未ログイン時はnull)
    isAuthenticated: boolean;        // 認証済みかどうか
    isLoading: boolean;              // 認証処理中かどうか(ローディング表示用)
    error: Error | null | undefined; // 認証エラー情報(エラーがない場合はnullまたはundefined)
}

ステップ2. Cognito接続設定(authConfig.ts)

次に、Cognitoとの接続に必要な設定を定義します。
作成済みのCognitoアプリケーションクライアントに合わせて、同じ設定にしてください。

この設定はreact-oidc-contextライブラリがCognitoのOIDCエンドポイントと通信するために使用します。

本番環境では環境変数で管理しておきましょう。

authConfig.ts
/** Cognito接続用の設定オブジェクト*/
export const authConfig = {
  // CognitoユーザープールのOIDCエンドポイントURL
  authority: 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}',
  // Cognitoアプリクライアントの識別子
  client_id: 'xxxxxxxxxxxxxxx',
  // 認証成功後にユーザーが戻ってくるURL (作成したCloudFrontのドメイン)
  redirect_uri: 'https://xxxxxxxxxxxxx.cloudfront.net/',
  // OAuth 2.0のフロータイプを指定 (codeまたはtoken)
  response_type: 'code',
  // アクセス要求する情報の範囲 (必須: openid)
  scope: 'openid profile email',
};

cognito_09.png

ステップ3. 認証コンテキストの定義 (authContext.ts)

認証情報は多くの画面で使用されることが考えられるため、React Context APIを使ってアプリ全体に共有する仕組みを作ります。
Contextを使うことで、propsの受け渡しなしで認証状態へアクセスできるようになります。

authContext.ts
import { createContext } from 'react';

import type { AuthState } from './type';

/** 認証コンテキストの型定義 */
interface AuthContextType extends AuthState {
    signIn: () => void;   // Cognitoのサインインページへリダイレクトする関数
    signOut: () => void;  // サインアウトしてCognitoのサインアウトページへリダイレクトする関数
}

/** 認証情報とメソッドを提供するReact Context */
export const AuthContext = createContext<AuthContextType | undefined>(undefined)

ステップ4. 認証プロバイダーの実装 (AuthProvider.tsx)

認証機能の中核となるProviderコンポーネントを実装します。
このコンポーネントは、react-oidc-context と連携し、Cognito認証の状態を把握してユーザー情報を生成します。

AuthProviderは二層構造で設計されています。

  1. 外側の層(AuthProvider): react-oidc-contextのProviderをセットアップ
  2. 内側の層(AuthContextProvider): アプリケーション固有のロジックを実装
AuthProvider.tsx
import { useCallback, useEffect, useState, type ReactNode } from 'react';

import { useAuth as useOidcAuth, AuthProvider as OidcProvider } from 'react-oidc-context';

import { authConfig } from './authConfig';
import { AuthContext } from './authContext';

import type { User } from './type';

/** 認証プロバイダーのメインコンポーネント */
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  return (
    // OIDCプロバイダーでラップ(Cognito接続の基盤)
    <OidcProvider {...authConfig}>
      {/* アプリ独自の認証コンテキストプロバイダー */}
      <AuthContextProvider>{children}</AuthContextProvider>
    </OidcProvider>
  );
};

/** 内部的な認証コンテキストプロバイダー */
const AuthContextProvider = ({ children }: { children: ReactNode }) => {
  // react-oidc-contextの認証フックを使用
  const oidcAuth = useOidcAuth();
  
  // アプリで使用するユーザー情報の状態管理
  const [user, setUser] = useState<User | null>(null);

  // 開発環境かどうかのフラグ(開発環境では認証をスキップ)
  const isAuthEnabled = import.meta.env.MODE !== 'development';

  // OIDC認証状態の変化を監視し、ユーザー情報を更新
  useEffect(() => {
    // 開発モードの場合はダミーユーザーを設定(テスト用)
    if (!isAuthEnabled) {
      setUser({
        username: 'devuser',
        email: 'devuser@example.com',
        idToken: 'dev-id-token',
        accessToken: 'dev-access-token',
      });
      return;
    }

    // 認証済みかつユーザー情報が存在する場合
    if (oidcAuth.isAuthenticated && oidcAuth.user) {
      // 認証成功後、URLに残る認可コードとstateパラメータをクリーンアップ
      // (URLを綺麗に保つため、0.5秒後に実行)
      const timer = setTimeout(() => {
        if (window.location.search.includes('code=') || window.location.search.includes('state=')) {
          // ブラウザの履歴を更新してパラメータを削除
          window.history.replaceState({}, '', window.location.pathname);
        }
      }, 500);

      // OIDCユーザー情報からトークンを取得
      const idToken = oidcAuth.user.id_token || '';
      const accessToken = oidcAuth.user.access_token || '';

      // IDトークン(JWT)をデコードしてユーザー情報を抽出
      // JWTは「ヘッダー.ペイロード.署名」の形式なので、[1]でペイロード部分を取得
      const decodedIdToken = JSON.parse(atob(idToken.split('.')[1]));
      
      // Cognitoから取得できるユーザー名とメールアドレスを設定
      const username = decodedIdToken['cognito:username'] || decodedIdToken.email || 'unknown';
      const email = decodedIdToken.email || 'unknown@example.com';

      // アプリ用のユーザーオブジェクトを作成
      const userData: User = {
        username,
        email,
        idToken,      // APIリクエスト時の認証に使用
        accessToken   // APIアクセス時の認可に使用
      };
      
      setUser(userData);
      
      // クリーンアップ関数でタイマーをクリア
      return () => clearTimeout(timer);
    } else {
      // 未認証の場合はユーザー情報をnullに設定
      setUser(null);
    }
  }, [isAuthEnabled, oidcAuth.isAuthenticated, oidcAuth.user]);

  // サインイン処理
  // Cognitoのサインインページへリダイレクト
  const signIn = useCallback(() => {
    oidcAuth.signinRedirect();
  }, [oidcAuth]);

  // サインアウト処理
  // Cognitoのサインアウトページへリダイレクトし、セッションをクリア
  const signOut = useCallback(() => {
    // Cognito情報 (環境変数から読み込むようにするとよい)
    const cognitoDomain = 'https://ap-northeast-1xxxxxxxxx.auth.ap-northeast-1.amazoncognito.com';
    const clientId = 'xxxxxxxxxxxxxxx';
    const responseType = 'code';
    const scope = 'openid profile email';
    const redirectUri = 'https://xxxxxxxxxxxxx.cloudfront.net/';

    // CognitoログアウトURL
    window.location.href = `${cognitoDomain}/logout?client_id=${clientId}&response_type=${responseType}&scope=${scope}&redirect_uri=${encodeURIComponent(
      redirectUri,
    )}`;
    
  }, []);

  // 認証コンテキストの値を提供
  return (
    <AuthContext.Provider
      value={{
        user,                                                              // ユーザー情報
        isAuthenticated: isAuthEnabled ? oidcAuth.isAuthenticated : true,  // 認証状態(開発環境では常にtrue)
        isLoading: isAuthEnabled ? oidcAuth.isLoading : false,             // ローディング状態
        error: isAuthEnabled ? oidcAuth.error : null,                      // エラー情報
        signIn,                                                            // サインイン関数
        signOut,                                                           // サインアウト関数
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

ステップ5. カスタムフックの作成 (useAuth.ts)

認証情報を取得するときにContextに毎回アクセスするのは面倒なので、使いやすいフックを作成します。

useAuth.ts
import { useContext } from "react";

import { AuthContext } from "./authContext";

/** 認証情報にアクセスするためのカスタムフック */
export const useAuth = () => {
    // AuthContextから値を取得
    const context = useContext(AuthContext);
    
    // contextがundefinedの場合、AuthProviderでラップされていない
    if (!context) {
        throw new Error("useAuth must be used within an AuthProvider");
    }
    
    // 認証情報と関数を返す
    return context;
}

ステップ6. 認証ガードの作成 (AuthGuard.tsx)

特定の画面を「認証済みのユーザーだけ」に見せたい場合に使用するコンポーネントを作成します。

useAuthから認証情報を取得し、その状態に応じて表示させる内容を切り替えます。

AuthGuard.tsx
import type { ReactNode } from 'react';
import { useEffect } from 'react';

import { useAuth } from './useAuth';

/** 認証ガードコンポーネント */
export const AuthGuard = ({ children }: { children: ReactNode }) => {
  // 認証フックから必要な情報を取得
  const { isAuthenticated, isLoading, error, signIn } = useAuth();
  
  // 環境に応じて認証を有効/無効にする(開発環境では無効)
  const isAuthEnabled = import.meta.env.MODE !== 'development';

  useEffect(() => {
    // 開発環境では認証をスキップ(すぐに子コンポーネントを表示)
    if (!isAuthEnabled) {
      return;
    }

    // 本番環境で未認証状態の場合、自動的にサインインページへリダイレクト:
    if (!isLoading && !isAuthenticated && !error) {
      signIn();
    }
  }, [isAuthEnabled, isAuthenticated, isLoading, error, signIn]);

  // 開発環境:認証をスキップして子コンポーネントを表示
  if (!isAuthEnabled) {
    return <>{children}</>;
  }

  // 認証処理中:ローディング表示
  if (isLoading) {
    return <div>Loading...</div>;
  }

  // エラー発生時:エラーメッセージを表示
  if (error) {
    return <div>{`Error: ${error.message}`}</div>;
  }

  // 未認証状態:サインインページへのリダイレクト待ち
  if (!isAuthenticated) {
    return <div>Loading...</div>;
  }

  // 認証済み:子コンポーネントを表示
  return <>{children}</>;
};

ステップ7. アプリ全体への組み込み

最後に、作成した認証機能をアプリに統合します。

エントリーポイント (main.tsx)

アプリ全体をAuthProviderでラップすることで、すべてのコンポーネントから認証機能にアクセスできるようになります。

main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

import './index.css'
import App from './App.tsx'
import { AuthProvider } from './auth/AuthProvider.tsx'

/** アプリケーションのエントリーポイント */
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    {/* AuthProviderでアプリ全体をラップし、認証機能を提供 */}
    <AuthProvider>
      {/* メインアプリケーションコンポーネント */}
      <App />
    </AuthProvider>
  </StrictMode>,
)

メインアプリケーション (App.tsx)

画面全体をAuthGuardでラップして保護します。
また、useAuthから呼び出したユーザー情報の表示とサインアウトボタンを実装します。

App.tsx
import { AuthGuard } from './auth/AuthGuard';
import { useAuth } from './auth/useAuth';
import './App.css';

/** メインアプリケーションコンポーネント */
function App() {
  // ユーザー情報、サインアウト関数の呼び出し
  const { user, signOut } = useAuth();

  // レンダリング
  return (
    <AuthGuard>
      <div style={{ padding: '40px' }}>
        <h1>認証成功</h1>
        
        {/* ユーザー情報の表示 */}
        {user && (
          <div>
            <p>ユーザー名: {user.username}</p>
            <p>メールアドレス: {user.email}</p>

            {/* サインアウトボタン */}
            <button 
              onClick={signOut}
              style={{backgroundColor: '#dc3545', color: 'white'}}
            >
              サインアウト
            </button>
          </div>
        )}
      </div>
    </AuthGuard>
  );
}

export default App;

再デプロイして動作確認

認証機能を組み込んだら、アプリをビルドしてS3に再アップロードします。
S3の古いファイルは削除し、完全に置き換えるようにしてください。

$ npm run build

# S3アップロード (もしくは手動でアップロード)
$ aws s3 sync dist/  s3://react-auth-app/ --delete

S3へのアップロード完了後、CloudFrontドメイン (https://{distribution-name}.net) にアクセスして、アプリ内容が変更されているか確認します。

サインインページに自動リダイレクトされ、認証情報入力後に以下のような画面になったら成功です。

app_04.png

サインアウトボタンをクリックすると、サインアウト処理が行われ、サインインページに飛ばされます。

app_01.png

以上でReactアプリ側のユーザー認証の実装は完了です。

ユーザー認証のトラブルシューティング

よくある問題と解決方法

1. 無限リダイレクトが発生する場合

  • Cognito のアプリクライアント設定で、リダイレクト URI が正しく設定されているか確認してください
  • 開発環境では http://localhost:5173、本番環境では実際のドメインを設定してください

2. トークンのデコードでエラーが発生する場合

  • ID トークンが正しく取得できているか確認してください
  • Cognito のスコープに openid が含まれているか確認してください

3. サインアウトが正しく動作しない場合

  • Cognito のサインアウト URL が正しく設定されているか確認してください
  • サインアウト時にWebストレージのトークンもクリアされているか確認してください

4. 認証付きAPIを作成

前のセクションでReactアプリにCognitoを使った認証機能を組み込みました。
このセクションでは、認証されたユーザーのみがアクセスできるAPIを作成し、Lambda + API Gatewayにデプロイしていきます。

architecture_04.png

APIアプリケーションの作成

まずは、Lambdaに載せるAPIアプリを作成します。

プロジェクト構成

今回は最小限の構成で、以下の2つのファイルだけ作成します。

src/
  ├── main.py
  └── requirements.txt

依存パッケージ

requirements.txt
fastapi==0.122.0
mangum==0.19.0
  • FastAPI:高速でモダンなPython Webフレームワーク
  • Mangum:FastAPIをLambda上で動作させるためのアダプター

Lambdaは event, context という独自の形式でリクエストを受け取るため、その形式とFastAPIの形式の変換を行ってくれるのがMangumです。

APIの実装

/helloエンドポイントのみのシンプルなAPIを実装します。

main.py
from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

@app.get("/hello")
def hello():
    return {"message": "Hello World"}

# Lambdaが呼び出すハンドラ
handler = Mangum(app)

Lambda用デプロイパッケージの作成

次に、この FastAPI アプリを Lambda にデプロイできる形にまとめます。
コードと依存ライブラリをひとつの zip ファイルに固めます。

Windowsでデプロイパッケージを作成する場合、Lambda上でPythonライブラリのインポートに失敗することがあります。その場合はLinux環境を用意して作業するか、Lambda Layer の使用を検討してください。

# パッケージ用の一時ディレクトリを作成
$ mkdir package

# 依存ライブラリをpackageにインストール
$ pip install -r requirements.txt -t ./package

# アプリケーションコードをpackageにコピー
$ cp main.py ./package

# packageの中身をzip化
$ cd package
$ zip -r ../deployment_package.zip .

ポイント

  • zipファイルは「packageフォルダごと」ではなく、中身だけをzip化してください
  • main.pyがzipファイルのルートレベルに配置されるようにしてください

Lambda関数の作成

続いて、作成したパッケージをデプロイするためのLambda関数を用意します。

  • 関数名:任意 (例:fastapi-hello)
  • ランタイム:Python3.x (開発環境と同じバージョンを指定)
  • アーキテクチャ:x86_64

lambda_01.png

作成した関数に先ほどのzipファイルをアップロードします。

  • 「コード」タブ >「アップロード元」> 「.zipファイルをアップロード」を選択
  • 作成したdeployment_package.zipを選択してアップロード

lambda_02.png

次に、Lambdaが正しいエントリーポイント (main.handler) を見つけられるように、ハンドラ名を設定します。

  • 「コード」タブ > 「ランタイム設定」を編集
  • ハンドラをmain.handlerに変更 (<ファイル名>.<ハンドラ名>の形式)

lambda_03.png

これでLambda関数の作成ができました。
API Gateway から呼び出されることを想定して、テストイベントを使って動作確認しておきます。

  • 「テスト」タブから「新しいイベントを作成」を選択
  • テンプレート:API Gateway AWS Proxy
  • イベントJSONのpathhttpMethodを以下のように変更して「テスト」実行
"path": "/hello",
"httpMethod": "GET",

lambda_04.png

lambda_05.png

成功すると、レスポンスボディに {\"message\":\"Hellow World\"} が表示されます。

lambda_06.png

API Gatewayの作成

Lambda関数を外部に公開するため、API Gatewayを設定します。

「APIの作成」から、「HTTP API」の「構築」をクリック

api_gateway_01.png

以下のように設定していきます。

  • 統合タイプ:Lambda
  • 統合先:作成したLambda関数

api_gateway_02.png

ルート設定

  • メソッド:GET
  • パス:/hello

api_gateway_03.png

ステージ定義

  • ステージ名:任意
  • 自動デプロイ::ONのままでよいです

api_gateway_04.png

内容を確認して作成します。

api_gateway_05.png

作成が完了すると、APIの概要画面にエンドポイントURLが表示されます。
そのURL+/helloにGETリクエストを送って、Lambdaのレスポンスが返ってくるか確認します。(ブラウザでURLを直接開いても確認できます。)

$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/hello

{"message":"Hello World"}  # 成功

これで、API Gatewayを経由してLambdaを公開することができました。
この時点では「誰でも叩ける公開API」の状態です。

Cognito認証の追加

最後に、Cognitoで認証されたユーザーだけがこのAPIを呼べるように設定します。

作成したAPI Gatewayのオーソライザーの設定画面から、/helloエンドポイントに対して「オーソライザーを作成」します

  • タイプ:JWT
  • 名前:任意 (例:cognito-auth)
  • 発行者URL:作成したCognitoユーザープールの Issuer URL
    (https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXXXXXX)
  • 対象者:Reactアプリで使用しているCognitoアプリケーションクライアントID

api_gateway_06.png

これでCognito連携ができました。
先ほどと同じURLにアクセスして、メッセージを確認します。

$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/hello

{"message":"Unauthorized"}  # 未認証

「Unauthorized (未認証)」のメッセージが返ってくれば、認証無しのアクセスはブロックできているということです。

このAPIを実際に呼び出すには、リクエストヘッダーにCognitoが発行したJWTを付ける必要があります。

$ curl -H "Authorization: Bearer <Cognitoで発行されたJWT>" \
  https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/hello

{"message":"Hello World"}  # 認証されて実行成功

これで、Cognitoで認証されたユーザーだけがアクセスできるAPIが完成しました。
次のセクションでは、Reactアプリ側からこのAPIを呼び出して連携させていきます。

5. Reactアプリ内でAPIを実行

作成した認証付きAPIを、Reactアプリ内から実行してみます。

App.tsxを以下のように改造します。

App.tsx
import { useState } from 'react';

import { AuthGuard } from './auth/AuthGuard';
import { useAuth } from './auth/useAuth';

import './App.css';

function App() {
  const { user, signOut } = useAuth();

  // 状態管理
  const [apiResponse, setApiResponse] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  // APIエンドポイント(環境変数から読み込むとよい)
  const API_URL = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/hello';

  // APIを呼び出す
  const callApi = async () => {
    if (!user?.idToken) {
      alert('IDトークンが見つかりません');
      return;
    }

    setIsLoading(true);
    setApiResponse('API実行中...');

    try {
      const response = await fetch(API_URL, {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${user.idToken}`,
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        throw new Error(`APIエラー: ${response.status}`);
      }

      const data = await response.json();
      setApiResponse(JSON.stringify(data, null, 2));
    } catch (error) {
      alert(error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <AuthGuard>
      <div style={{ padding: '40px' }}>
        <h1>認証成功</h1>

        {/* ユーザー情報の表示 */}
        {user && (
          <div>
            <p>ユーザー名: {user.username}</p>
            <p>メールアドレス: {user.email}</p>

            {/* サインアウト実行 */}
            <button onClick={signOut} style={{ backgroundColor: '#dc3545', color: 'white' }}>
              サインアウト
            </button>
          </div>
        )}
      </div>

      {/* API呼び出し */}
      <div>
        <button
          onClick={callApi}
          style={{ backgroundColor: '#007bff', color: 'white' }}
          disabled={isLoading}
        >
          API呼び出し
        </button>

        {/* レスポンス表示 */}
        <div
          style={{
            marginTop: '20px',
            height: '60px',
            backgroundColor: '#d4edda',
            border: 'solid',
            alignContent: 'center',
          }}
        >
          {apiResponse}
        </div>
      </div>
    </AuthGuard>
  );
}

export default App;

変更後、ビルドしてS3にアップロードしなおします。

アプリにログイン後、「API呼び出し」ボタンを押すと、Cognitoで発行されたIDトークンがヘッダーに付与され、LambdaのAPIを呼ぶことができます。

app_05.png

成功すれば、APIから返ってきたJSONが画面に表示されます。

app_06.png

これで、Reactアプリ → Cognito → API Gateway → Lambda という、本記事で構築してきた一連の仕組みがすべて完成しました。

まとめ

本記事では、React (S3 + CloudFront)、Cognito、API Gateway、Lambdaを組み合わせて、認証付きWebアプリケーションの基本構成を構築しました。

  • フロントエンドをS3 + CloudFrontでホスティング
  • Cognitoでログインを実装し、アプリ内で認証状態を管理
  • API Gateway + Lambdaによる認証付きAPIを作成
  • React からIDトークンを使ってAPIを呼び出し

という流れを順番に追うことで、AWSを使用した「認証つきSPA × API」の最小構成を体験できたと思います。
次のステップとして、UI・APIの充実や、Route53による独自ドメイン運用、WAFの強化、CI/CD導入などもぜひ試してみてください。

この記事が、同様の構成を作ろうとしている方の参考になれば幸いです。

参考

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?