mcpField
B
betterauth-fastapi-jwt-bridge

betterauth-fastapi-jwt-bridge

github.com/bilalmkセキュリティ141 回インストール39 週間前に更新
リポジトリ

betterauth-fastapi-jwt-bridge について

Better Auth(Next.js)とFastAPIのJWT認証連携を支援するスキルです。JWKSによるトークン検証とAPIルートの保護に加え、UUIDでのユーザー識別、フロントエンド連携、問題調査を扱います。

主な機能

  1. 01JWKSによるJWT検証
  2. 02FastAPI認証依存関係の設定
  3. 03ユーザー識別とルート権限の確認
  4. 04JWTのカスタムUUIDクレーム対応
  5. 05検証スクリプトとテンプレート

活用シーン

  1. 01Better AuthのログインをFastAPIに接続
  2. 02APIルートにユーザー単位のアクセス制御を追加
  3. 03JWKS、トークン、UUID連携の問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bilalmk/todo_correct --skill betterauth-fastapi-jwt-bridge

インストール後、Claude Code で/betterauth-fastapi-jwt-bridgeを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/
api_client.ts
auth_dependencies.py
better_auth_migrations.py
jwt_verification.py
references/
jwks-approach.md
security-checklist.md
troubleshooting.md
scripts/
test_jwt_verification.py
verify_jwks.py

詳細

Better Auth + FastAPI JWT ブリッジ

JWKS 検証を使用して、安全なステートレス認証を実現する Better Auth(Next.js)と FastAPI 間の本番環境対応 JWT 認証を実装します。

アーキテクチャ

User Login (Frontend)
    ↓
Better Auth → Issues JWT Token
    ↓
Frontend API Request → Authorization: Bearer <token>
    ↓
FastAPI Backend → Verifies JWT with JWKS → Returns filtered data

クイックスタートのワークフロー

ステップ 1:Better Auth で JWT を有効にする(フロントエンド)
// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [jwt()],  // Enables JWT + JWKS endpoint
    // ... other config
})

データベースのマイグレーションが必要です:

JWT プラグインを追加した後、必要なテーブルを作成するためにマイグレーションを実行します。

# Next.js (Better Auth CLI)
npx @better-auth/cli migrate

⚠️ 重要 — 2 つのテーブルが別々に必要です:

  1. session テーブルには token カラムが必要です(Better Auth のコア要件)

  2. jwks テーブルが必要です(JWT プラグインの要件)

これらは別々のマイグレーションです。JWT プラグインは jwks テーブルを作成しますが、session テーブルは変更しません。

ステップ 2:JWKS エンドポイントを確認する

JWKS エンドポイントが動作していることをテストします。

python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks
ステップ 3:バックエンドの検証を実装する

assets/ のテンプレートを FastAPI プロジェクトにコピーします。

  • assets/jwt_verification.py → backend/app/auth/jwt_verification.py
  • assets/auth_dependencies.py → backend/app/auth/dependencies.py

依存関係をインストールします。

pip install fastapi python-jose[cryptography] pyjwt cryptography httpx
ステップ 4:API ルートを保護する
from app.auth.dependencies import verify_user_access

@router.get("/{user_id}/tasks")
async def get_tasks(
    user_id: str,
    user: dict = Depends(verify_user_access)
):
    # user_id is verified to match authenticated user
    return get_user_tasks(user_id)
ステップ 5:フロントエンドの API クライアントを設定する

assets/api_client.ts を frontend/lib/api-client.ts にコピーして、次のように使用します。

import { getTasks, createTask } from "@/lib/api-client"

const tasks = await getTasks(userId)

⚠️ React コンポーネントのパターン:

Better Auth は useSession() フックを提供していません。useEffect で authClient.getSession() を使用してください。

import { useState, useEffect } from "react"
import { authClient } from "@/lib/auth-client"

function MyComponent() {
  const [user, setUser] = useState(null)

  useEffect(() => {
    async function loadSession() {
      const session = await authClient.getSession()
      if (session?.data?.user) {
        setUser(session.data.user)
      }
    }
    loadSession()
  }, [])

  return <div>Welcome {user?.name}</div>
}

詳しい例については、フロントエンド統合の問題を参照してください。

Better Auth UUID 統合(ハイブリッド ID アーキテクチャ)

解決する問題: Better Auth は内部で String ID を使用しますが、アプリケーションでは API ルートやデータベースの外部キー間で型の一貫性を保つために UUID が必要となることがよくあります。

解決策: ハイブリッド ID 方式を採用します。User テーブルに id(String、Better Auth の要件)と uuid(UUID、アプリケーションで使用)の両方を持たせます。

データベーススキーマ
CREATE TABLE "user" (
    id VARCHAR PRIMARY KEY,              -- Better Auth String ID
    uuid UUID UNIQUE NOT NULL,           -- Application UUID ⭐
    email VARCHAR UNIQUE NOT NULL,
    "emailVerified" BOOLEAN DEFAULT FALSE,
    name VARCHAR,
    "createdAt" TIMESTAMP NOT NULL,
    "updatedAt" TIMESTAMP NOT NULL
);

-- UUID auto-generated by database
ALTER TABLE "user" ALTER COLUMN uuid SET DEFAULT gen_random_uuid();

-- All foreign keys point to user.uuid
CREATE TABLE tasks (
    id UUID PRIMARY KEY,
    user_id UUID REFERENCES "user"(uuid) ON DELETE CASCADE,  -- ⭐ FK to uuid
    title VARCHAR NOT NULL,
    ...
);
フロントエンドの設定(Better Auth)

UUID 生成フックと JWT カスタムクレームを追加します。

// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
import { Pool } from "pg"

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

export const auth = betterAuth({
  database: pool,

  // Hook to fetch database-generated UUID
  hooks: {
    user: {
      created: async ({ user }) => {
        const result = await pool.query(
          'SELECT uuid FROM "user" WHERE id = $1',
          [user.id]
        )
        const uuid = result.rows[0]?.uuid
        return { ...user, uuid }
      }
    }
  },

  // Include UUID in JWT payload
  plugins: [
    jwt({
      algorithm: "EdDSA",
      async jwt(user, session) {
        return {
          uuid: user.uuid,  // ⭐ Custom claim for backend
        }
      },
    }),
  ],
})
バックエンドのパターン(FastAPI)

sub ではなく、JWT カスタムクレームから UUID を取得します。

# backend/app/auth/dependencies.py
from uuid import UUID

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    payload = verify_jwt_token(token)

    # Extract UUID from custom claim (not 'sub')
    user_uuid_str = payload.get("uuid")  # ⭐
    user_uuid = UUID(user_uuid_str)

    # Query by UUID
    user = await session.execute(
        select(User).where(User.uuid == user_uuid)
    )
    return user.scalar_one_or_none()

async def verify_user_match(
    user_id: UUID,  # From URL path
    current_user: User = Depends(get_current_user)
) -> User:
    # Compare UUIDs (not String IDs)
    if current_user.uuid != user_id:
        raise HTTPException(403, "Not authorized")
    return current_user

重要なパターン: 常に User.uuid で検索し、JWT カスタムクレームから取得した UUID と照合してください。

主なコンポーネント

1. JWKS 検証フロー
  1. Better Auth のエンドポイントから JWKS を取得する(キャッシュ済み)
  2. JWT トークンのヘッダーから kid(キー ID)を取得する
  3. kid を使って JWKS 内から一致する公開鍵を探す
  4. Ed25519 公開鍵を使って署名を検証する
  5. クレーム(発行者、オーディエンス、有効期限)を検証する
  6. ペイロードからユーザー情報(sub クレーム)を取得する
2. ユーザー分離パターン

JWT の user_id が URL 内の user_id と一致することを必ず検証します。

if current_user["user_id"] != user_id:
    raise HTTPException(status_code=403, detail="Not authorized")

これにより、他のユーザーのデータにアクセスできないようにします。

3. JWT ペイロードの構造(UUID 統合に対応)
{
  "sub": "user_abc123",       // Better Auth String ID
  "uuid": "a1b2c3d4-e5f6...", // Application UUID (custom claim) ⭐
  "email": "[email protected]",
  "name": "User Name",
  "iat": 1234567890,          // Issued at
  "exp": 1234567890,          // Expiration
  "iss": "http://localhost:3000",
  "aud": "http://localhost:3000"
}

重要: uuid カスタムクレームは、バックエンドでのユーザー識別とデータベース検索に使用されます。 Better Auth は String ID(sub)でユーザーを管理し、アプリケーションは型の一貫性を保つために UUID(uuid)を使用します。

環境設定

フロントエンド(.env.local):

BETTER_AUTH_SECRET="min-32-chars-secret"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:8000"

バックエンド(.env):

BETTER_AUTH_URL="http://localhost:3000"
DATABASE_URL="postgresql://..."

テストと検証

JWKS エンドポイントをテストする
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks

期待される出力には、kid、kty、crv、x フィールドを含む公開鍵が表示されます。

JWT 検証をテストする
python scripts/test_jwt_verification.py \
  --jwks-url http://localhost:3000/api/auth/jwks \
  --token "eyJhbGci..."

トラブルシューティング

認証に関する問題
問題解決策
"relation 'jwks' does not exist"JWKS テーブルのマイグレーションを作成します。データベーススキーマに関する問題を参照してください
"column 'token' does not exist"セッションテーブルに token 列を追加します。データベーススキーマに関する問題を参照してください
"Token missing UUID (uuid claim)"Better Auth フックと JWT プラグインを設定します。UUID 統合に関する問題を参照してください
"User not found after registration"二重認証システムの競合です。UUID 統合に関する問題を参照してください
"authClient.useSession is not a function"useEffect 内で authClient.getSession() を使用します。フロントエンド統合に関する問題を参照してください
"No authentication token available"session.session.token ではなく session.data.session.token を使用します。フロントエンド統合に関する問題を参照してください
"Unable to find matching signing key"jwt_verification.py の JWKS キャッシュをクリアします
"Token has expired"フロントエンドでセッションを更新する必要があります
"Invalid token claims"発行者/オーディエンスが BETTER_AUTH_URL と一致していることを確認します
403 Forbidden (UUID mismatch)String と UUID の比較ではなく、UUID 同士を比較するようにします。UUID 統合に関する問題を参照してください
フロントエンドとバックエンドの統合に関する問題(新規 - 2026-01-02)
問題根本原因解決策
200 OK なのにタスクが表示されないバックエンドは配列を返すが、フロントエンドはページネーションされたオブジェクトを想定しているArray.isArray() チェックで両方の形式に対応します。詳しくはフロントエンドとバックエンドの統合を参照してください
タグのフィルタリングでクラッシュするバックエンドはタグオブジェクト {id, name, color} を返すが、フロントエンドは number[] を想定しているTypeScript の型を Pydantic スキーマに合わせて更新します。詳しくはタグのフィルタリングを参照してください
ページネーションに「NaN」と表示されるnull チェックなしで、オプションの priority フィールドを算術演算に使用しているオプションフィールドにデフォルト値を設定し、null チェックを追加します。詳しくは優先度のソートを参照してください
タグがデータベースに保存されないTaskCreate スキーマが tags フィールドを受け付けない複数ステップの操作を使い、タスクを作成してからタグを割り当てます。詳しくはタグの割り当てを参照してください
編集フォームのフィールドが空白になる非制御コンポーネント、フィールド名の不一致、日時形式制御コンポーネントを使い、フィールド名を一致させ、日時を変換します。詳しくは編集フォームを参照してください
500 エラー:タイムゾーンの比較タイムゾーン情報のない日時とタイムゾーン情報のある日時を比較している比較前に両方を UTC に正規化します。詳しくはタイムゾーンの修正を参照してください
タグの色の検証に失敗するフロントエンドでは色が必須だが、バックエンドではオプションZod スキーマで色をオプションにし、デフォルト値を設定します。詳しくはタグの色を参照してください
タグフィルターのチェックボックスが動作しないバックエンドは id: number を返すが、FilterContext は string[] を使用している比較用に ID を文字列に変換します。詳しくはタグフィルターを参照してください

📚 重要な資料: コード例と詳細な修正方法については、トラブルシューティングガイドのフロントエンドとバックエンドの統合に関する問題セクションを参照してください。このセクションでは、実装中に見つかった8つの重大な問題と、その解決方法を説明しています。

主な教訓:

  1. フロントエンドの型を書く前に、必ずバックエンドの Pydantic スキーマを読む
  2. オプションのフィールドには null チェックとデフォルト値を使用する
  3. 値が事前入力されたフォームには制御コンポーネントを使用する
  4. フロントエンドとバックエンドでフィールド名を正確に一致させる
  5. モックデータではなく、実際のバックエンドのレスポンスでテストする

詳しい解決策と予防策については、references/troubleshooting.md を参照してください。

高度なトピック

JWKS のキャッシュ戦略

実装では @lru_cache を使用して JWKS のレスポンスをキャッシュします。

  • トークンの kid が不明な場合はキャッシュを無効化します
  • 公開鍵が変更されることはほとんどないため、安全にキャッシュできます
  • Better Auth へのネットワーク呼び出しを削減します

実装の詳細については、references/jwks-approach.md を参照してください。

セキュリティチェックリスト

本番環境に移行する前に、以下を確認してください。

  • ✅ すべての API 呼び出しで HTTPS のみを使用する
  • ✅ トークンの有効期限を検証する
  • ✅ 発行者とオーディエンスのクレームを検証する
  • ✅ ユーザー ID による認可を適用する
  • ✅ CORS を適切に設定する
  • ✅ エラーメッセージから機密情報が漏れないようにする

完全なリストについては、references/security-checklist.md を参照してください。

リソース

scripts/
  • verify_jwks.py - JWKS エンドポイントの利用可能性をテストする
  • test_jwt_verification.py - JWT トークンの検証を確認する
references/
  • jwks-approach.md - JWKS 実装の詳細ガイド
  • security-checklist.md - 本番環境のセキュリティ要件
  • troubleshooting.md - よくある問題とその修正方法
assets/
  • jwt_verification.py - 完全な JWKS 検証モジュールのテンプレート
  • auth_dependencies.py - FastAPI の依存関係のテンプレート
  • api_client.ts - フロントエンド API クライアントのテンプレート
  • better_auth_migrations.py - Better Auth テーブル用の Alembic マイグレーションテンプレート(トークン列の修正を含む)

共有シークレットより JWKS を選ぶ理由

項目JWKS共有シークレット
セキュリティ✅ 非対称(より安全)⚠️ 対称(安全性が低い)
スケーラビリティ✅ 複数のバックエンド⚠️ シークレットの共有が必要
本番環境✅ 推奨⚠️ 開発環境のみ
複雑さ中程度シンプル

推奨事項: 本番環境では常に JWKS を使用してください。

コメント