シンプルなアプリ

以下は「React(SPA)でログイン中のユーザーだけが実行(ボタン押下など)できるシンプルなアプリ」の基本テンプレートです。Auth0 の @auth0/auth0-react を使い、認証済みかどうかでボタンを有効化/API 呼び出しを行います。ローカルで動く最小構成と、Auth0 のアクセストークンを取得して保護されたバックエンド API を呼ぶ例を含めています。

やったこと(この返信で用意したもの)

  • 最小限で動く React SPA のファイル群(Auth0 プロバイダ、ルーティング、保護されたページ、実行ボタン、API 呼び出しの実装例)
  • README にセットアップ手順(Auth0 設定、環境変数、起動方法) 次にできること(必要なら)
  • 実際のバックエンド(Express)と連携するサンプルを追加
  • PKCE の理解やトークン検証の具体例(サーバ側)を付ける

ファイル一覧 — コードをそのままコピーして使えます。

{
  "name": "auth0-react-runner",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "react-router-dom": "^6.12.1",
    "@auth0/auth0-react": "^2.1.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
```markdown
# Auth0 + React (SPA) - シンプル実行アプリ

概要
- Auth0 を使った認証(@auth0/auth0-react)
- ログイン済みのユーザーだけが「実行」できるシンプル UI(ボタン)
- 必要に応じてアクセストークンを取得して保護された API を呼び出す例あり

前提(Auth0)
1. Auth0 ダッシュボードでアプリを作成(Single Page Application)
2. Allowed Callback URLs, Allowed Logout URLs に `http://localhost:3000` を登録
3. アプリの Client ID と Domain を控える
4. (API を呼ぶ場合)API を Auth0 で作成し、Identifier(例: `https://my-api.example.com`)を控える

環境変数(プロジェクトルートに `.env` を作る)
- REACT_APP_AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
- REACT_APP_AUTH0_CLIENT_ID=YOUR_CLIENT_ID
- REACT_APP_AUTH0_AUDIENCE=YOUR_API_IDENTIFIER (API を呼ぶ場合に設定)

セットアップ
1. 依存インストール
   npm install
2. 開発サーバ起動
   npm start
3. ブラウザで http://localhost:3000 を開く

動作
- 未ログイン時: ログインボタンを表示
- ログインすると「実行」ボタンが使える(ローカル実行と、アクセストークンを付けて保護 API を呼ぶ例がある)

次のステップ(必要なら)
- 実際のバックエンド(Express 等)を用意してアクセストークンの検証を追加
- トークンのスコープ/ロールに応じた表示制御
- Refresh Token Rotation の導入(長期セッションが必要な場合)

```javascript name=src/index.js
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { Auth0ProviderWithHistory } from "./auth/auth0-provider";
import { BrowserRouter as Router } from "react-router-dom";

const container = document.getElementById("root");
const root = createRoot(container);

root.render(
  <Router>
    <Auth0ProviderWithHistory>
      <App />
    </Auth0ProviderWithHistory>
  </Router>
);
```

```javascript name=src/auth/auth0-provider.jsx
import React from "react";
import { Auth0Provider } from "@auth0/auth0-react";
import { useNavigate } from "react-router-dom";

/*
  環境変数:
  - REACT_APP_AUTH0_DOMAIN
  - REACT_APP_AUTH0_CLIENT_ID
  - REACT_APP_AUTH0_AUDIENCE (任意, API 呼び出しをする場合)
*/

export const Auth0ProviderWithHistory = ({ children }) => {
  const navigate = useNavigate();

  const domain = process.env.REACT_APP_AUTH0_DOMAIN;
  const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
  const audience = process.env.REACT_APP_AUTH0_AUDIENCE;

  if (!domain || !clientId) {
    return <div>Auth0 の環境変数が設定されていません。</div>;
  }

  const onRedirectCallback = (appState) => {
    navigate(appState?.returnTo || window.location.pathname);
  };

  return (
    <Auth0Provider
      domain={domain}
      clientId={clientId}
      authorizationParams={{
        redirect_uri: window.location.origin,
        audience: audience, // audience を指定すると access_token を取得可能
        scope: "openid profile email",
      }}
      onRedirectCallback={onRedirectCallback}
      useRefreshTokens={false}
    >
      {children}
    </Auth0Provider>
  );
};
```

```javascript name=src/App.jsx
import React from "react";
import { Routes, Route, Link } from "react-router-dom";
import Home from "./pages/Home";
import Dashboard from "./pages/Dashboard";
import { useAuth0 } from "@auth0/auth0-react";
import ProtectedRoute from "./components/ProtectedRoute";

export default function App() {
  const { isLoading } = useAuth0();

  if (isLoading) return <div>Loading auth...</div>;

  return (
    <div style={{ padding: 20 }}>
      <nav style={{ marginBottom: 20 }}>
        <Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          }
        />
      </Routes>
    </div>
  );
}
```

```javascript name=src/components/ProtectedRoute.jsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Navigate } from "react-router-dom";

/*
  ログイン済みでない場合はログインページ(Auth0 のログイン画面)にリダイレクトする簡易保護ルート。
  単純に要認証ページを実現するのに使います。
*/

export default function ProtectedRoute({ children }) {
  const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0();

  if (isLoading) return <div>Loading...</div>;

  if (!isAuthenticated) {
    // loginWithRedirect を呼んでからもどってくる実装にしても良いですが、
    // ここではシンプルにリダイレクト先を / にしてログインを促します。
    loginWithRedirect();
    return <div>Redirecting to login...</div>;
    // あるいは以下のように Navigate を使う:
    // return <Navigate to="/" replace />;
  }

  return children;
}
```

```javascript name=src/pages/Home.jsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";

export default function Home() {
  const { loginWithRedirect, logout, isAuthenticated, user } = useAuth0();

  return (
    <div>
      <h1>Auth0 React Runner(サンプル)</h1>
      {isAuthenticated ? (
        <div>
          <p>ようこそ、{user?.name || user?.email}</p>
          <button onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}>
            ログアウト
          </button>
        </div>
      ) : (
        <div>
          <p>ログインして実行ボタンを使ってみてください。</p>
          <button onClick={() => loginWithRedirect()}>ログイン</button>
        </div>
      )}
    </div>
  );
}
```

```javascript name=src/pages/Dashboard.jsx
import React, { useState } from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { callProtectedApi } from "../utils/api";

/*
  Dashboard は「ログイン中に実行できる」ページの例です。
  - ログイン済みでないと来られない(ProtectedRoute で保護)
  - ローカルの実行(単純な処理)と、保護されたバックエンド API を呼ぶ例を含む
*/

export default function Dashboard() {
  const { getAccessTokenSilently } = useAuth0();
  const [log, setLog] = useState([]);

  const append = (msg) => setLog((l) => [msg, ...l]);

  const doLocalRun = () => {
    append(`実行: ローカル処理が成功しました (${new Date().toLocaleTimeString()})`);
  };

  const doProtectedApiCall = async () => {
    try {
      // API を呼ぶ場合、Auth0 の audience を設定していれば access token を取得可能
      const token = await getAccessTokenSilently();
      append("アクセストークン取得: 成功 (トークン長: " + token.length + ")");
      // 例: 実際の API URL を環境変数等で設定して呼ぶ
      const res = await callProtectedApi("/api/run", token);
      append("API 実行結果: " + JSON.stringify(res));
    } catch (e) {
      console.error(e);
      append("API 実行エラー: " + (e.message || e));
    }
  };

  return (
    <div>
      <h2>Dashboard(実行ページ)</h2>
      <div style={{ marginBottom: 12 }}>
        <button onClick={doLocalRun}>ローカル実行(ログに記録)</button>{" "}
        <button onClick={doProtectedApiCall}>保護 API を呼んで実行</button>
      </div>

      <div>
        <h3>ログ</h3>
        <ul>
          {log.map((l, i) => (
            <li key={i}>{l}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}
```

```javascript name=src/utils/api.js
/*
  保護された API を呼ぶためのヘルパー。
  - token を Authorization: Bearer <token> として渡す
  - サンプルでは同じオリジンの /api/run を叩く想定(実際はバックエンドの URL を指定してください)
*/

export async function callProtectedApi(path = "/api/run", token) {
  if (!token) throw new Error("アクセストークンがありません");

  const res = await fetch(path, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ action: "run" }),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`API エラー: ${res.status} ${text}`);
  }

  return res.json();
}
```

使用上のポイント(短く)
- SPA の場合は client_secret をクライアントに置かず、PKCE を用いるか @auth0/auth0-react の内部実装に従う
- Access Token を取得するには Auth0 のアプリ設定で Audience を設定し、Auth0Provider に audience を渡す必要があります
- API を呼ぶときはサーバ側でアクセストークン(JWT)の検証を行う(jwks を用いて署名検証、iss/aud/exp チェック)
- ローカルでテストする場合、バックエンドが無ければ callProtectedApi をモックするか、簡単な Express サーバを用意してください

以下で「React(SPA)フロント」と「保護された Express バックエンド」をつなぎ、Auth0 のアクセストークンを検証して保護されたエンドポイントを実行できる実装を提示します。やったことは:

- Express サーバーを作成し、@auth0/express-jwt + jwks-rsa で JWT(RS256)を検証するミドルウェアを組み込みました。
- /api/run エンドポイントを保護し、認証済みユーザーのみが実行できるようにしました。
- CORS 設定とエラーハンドリング(認証失敗時のレスポンス)を追加しました。
- フロント側の API 呼び出しヘルパーをバックエンド URL を使うように更新する例を用意しました。
- 必要な環境変数と実行手順の README を用意しました。

まずバックエンドのファイル群です。

```json name=backend/package.json
{
  "name": "auth0-express-api",
  "version": "0.1.0",
  "private": true,
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "@auth0/express-jwt": "^7.7.5",
    "jwks-rsa": "^3.0.1",
    "body-parser": "^1.20.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.22"
  }
}
```

```text name=backend/.env.example
# Auth0 設定
# 例:
# AUTH0_DOMAIN=dev-xxxxx.us.auth0.com
# AUTH0_AUDIENCE=https://my-api.example.com

AUTH0_DOMAIN=
AUTH0_AUDIENCE=

# サーバー設定
PORT=4000

# フロント側をホワイトリストに入れる(開発時)
CORS_ORIGIN=http://localhost:3000
```

```javascript name=backend/server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { expressjwt: jwt } = require('@auth0/express-jwt');
const jwksRsa = require('jwks-rsa');

const app = express();

const PORT = process.env.PORT || 4000;
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:3000';

if (!AUTH0_DOMAIN || !AUTH0_AUDIENCE) {
  console.error('Missing AUTH0_DOMAIN or AUTH0_AUDIENCE in environment. See .env.example');
  process.exit(1);
}

app.use(cors({
  origin: CORS_ORIGIN,
  methods: ['GET', 'POST', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(bodyParser.json());

// JWT ミドルウェア: JWKS から公開鍵を取得して署名検証
const checkJwt = jwt({
  // Provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint.
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    // JWKS endpoint
    jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`
  }),

  // Validate the audience and the issuer.
  audience: AUTH0_AUDIENCE,
  issuer: `https://${AUTH0_DOMAIN}/`,
  algorithms: ['RS256']
});

// 保護された実行エンドポイント
app.post('/api/run', checkJwt, (req, res) => {
  // express-jwt は署名検証後に req.auth をセットします
  const auth = req.auth || {};
  // ユーザー情報(sub, scope など)は auth に入る
  const userId = auth.sub || 'unknown';
  const now = new Date().toISOString();

  // 実行ロジック(ここは必要に応じて置き換え)
  const result = {
    message: 'Protected action executed successfully',
    executedAt: now,
    user: userId
  };

  res.json(result);
});

// ヘルスチェック(非保護)
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

// エラーハンドリング (認証エラーを整形して返す)
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    // express-jwt のエラー
    return res.status(401).json({
      error: 'invalid_token',
      error_description: err.message
    });
  }
  console.error(err);
  res.status(500).json({ error: 'server_error', error_description: err.message || 'Internal Server Error' });
});

app.listen(PORT, () => {
  console.log(`Auth0-protected API listening on http://localhost:${PORT}`);
  console.log(`Auth0 domain: ${AUTH0_DOMAIN}`);
  console.log(`Expected audience: ${AUTH0_AUDIENCE}`);
});
```

````markdown name=backend/README.md
```markdown
# Auth0-protected Express API (サンプル)

このバックエンドは Auth0 が発行した Access Token(JWT、RS256)を検証して保護されたエンドポイントを提供します。

主要エンドポイント
- GET  /api/health  (公開)
- POST /api/run     (保護、Authorization: Bearer <access_token> が必要)

事前準備(Auth0)
1. Auth0 Dashboard で「APIs」から API を作成します。
   - Identifier に任意の値(例: https://my-api.example.com)を設定します。
   - この Identifier をバックエンドの AUTH0_AUDIENCE に設定します。

2. SPA(Single Page Application)用の Application を作成し、
   - Allowed Callback URLs / Allowed Web Origins に `http://localhost:3000` を登録。
   - SPA から access token を取得するために、Auth0Provider に audience を渡します(フロント側の設定)。

セットアップ(ローカル)
1. コピーして .env を作成(backend/.env)
   - AUTH0_DOMAIN=your-tenant.auth0.com
   - AUTH0_AUDIENCE=https://my-api.example.com
   - PORT=4000
   - CORS_ORIGIN=http://localhost:3000

2. 依存インストール
   cd backend
   npm install

3. サーバ起動
   npm start
   (開発時は npm run dev)

フロントと接続する流れ
- フロント(SPA)で @auth0/auth0-react を使い、Auth0Provider に audience を渡すと getAccessTokenSilently() で access token が取得できます。
- 取得したトークンを Authorization: Bearer <token> ヘッダにのせて POST /api/run を呼びます。
- バックエンドは JWKS (/ .well-known/jwks.json) を使って署名検証し、aud/iss/exp をチェックしてリクエストを許可します。

テスト(フロントがない場合の簡易テスト)
- トークンは通常ユーザーのログインを通して取得するため、curl 単体でテストするには
  1) 事前にトークンを取得して(例: OAuth 認可コードで取得)、
  2) curl -X POST http://localhost:4000/api/run -H "Authorization: Bearer <ACCESS_TOKEN>" -d '{}'
- 正常なら 200 と JSON レスポンスが返ります。

セキュリティノート
- audience と issuer を正確に検査してください。
- 本番では CORS、ログ、レート制限、監査ログの設定を再確認してください。
```

次にフロント側での呼び出しを少しだけ更新します(既存プロジェクトに貼り付ける想定)。環境変数の追加例と API ヘルパーの更新です。

# React (SPA) 側の環境変数例
REACT_APP_AUTH0_DOMAIN=your-tenant.auth0.com
REACT_APP_AUTH0_CLIENT_ID=YOUR_CLIENT_ID
REACT_APP_AUTH0_AUDIENCE=https://my-api.example.com
# バックエンド API の URL (開発)
REACT_APP_API_URL=http://localhost:4000
/*
  フロントから保護 API を呼ぶヘルパー。

  前提:
  - Auth0Provider に audience を指定しておくこと
  - getAccessTokenSilently() でユーザーの access token を取得して渡す
  - REACT_APP_API_URL を .env に設定しておく
*/

const API_URL = process.env.REACT_APP_API_URL || '';

export async function callProtectedApi(path = "/api/run", token) {
  if (!token) throw new Error("アクセストークンがありません");

  const url = API_URL + path;

  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ action: "run" }),
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`API エラー: ${res.status} ${text}`);
  }

  return res.json();
}

使い方(全体の手順まとめ)

  1. Auth0 で API(Identifier = REACT_APP_AUTH0_AUDIENCE)を作成。
  2. SPA Application の Allowed Callback/Origins に http://localhost:3000 を設定。
  3. frontend の .env を作成:
    • REACT_APP_AUTH0_DOMAIN, REACT_APP_AUTH0_CLIENT_ID, REACT_APP_AUTH0_AUDIENCE, REACT_APP_API_URL
  4. backend の .env を作成:
    • AUTH0_DOMAIN, AUTH0_AUDIENCE (同じ値), PORT, CORS_ORIGIN
  5. 各ディレクトリで依存をインストールして起動:
    • cd backend && npm install && npm start
    • cd frontend && npm install && npm start
  6. ブラウザで http://localhost:3000 を開き、ログイン。Dashboard の「保護 API を呼んで実行」ボタンを押すと、フロントで getAccessTokenSilently() により取得したトークンがバックエンドに送られ、検証されたあと実行結果が返ります。

注意点 / トラブルシューティング(短く)

  • invalid_audience エラーが出る → フロント/バックエンドの audience(API Identifier)が一致しているか確認。
  • 401 が返る → token が送られていないか、期限切れ、または issuer/audience が違う可能性あり。トークンをデバッグするには jwt.io でデコードして iss/aud を確認。
  • CORS エラー → backend の CORS_ORIGIN を確認。