betterauth-fastapi-jwt-bridge について
Better Auth(Next.js)とFastAPIのJWT認証連携を支援するスキルです。JWKSによるトークン検証とAPIルートの保護に加え、UUIDでのユーザー識別、フロントエンド連携、問題調査を扱います。
主な機能
- 01JWKSによるJWT検証
- 02FastAPI認証依存関係の設定
- 03ユーザー識別とルート権限の確認
- 04JWTのカスタムUUIDクレーム対応
- 05検証スクリプトとテンプレート
活用シーン
- 01Better AuthのログインをFastAPIに接続
- 02APIルートにユーザー単位のアクセス制御を追加
- 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.tsauth_dependencies.pybetter_auth_migrations.pyjwt_verification.pyreferences/jwks-approach.mdsecurity-checklist.mdtroubleshooting.mdscripts/test_jwt_verification.pyverify_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 つのテーブルが別々に必要です:
-
sessionテーブルにはtokenカラムが必要です(Better Auth のコア要件)- エラー:
column "token" of relation "session" does not exist - 解決方法:データベーススキーマの問題を参照してください
- エラー:
-
jwksテーブルが必要です(JWT プラグインの要件)- エラー:
relation "jwks" does not exist - 解決方法:データベーススキーマの問題を参照してください
- エラー:
これらは別々のマイグレーションです。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.pyassets/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 検証フロー
- Better Auth のエンドポイントから JWKS を取得する(キャッシュ済み)
- JWT トークンのヘッダーから
kid(キー ID)を取得する kidを使って JWKS 内から一致する公開鍵を探す- Ed25519 公開鍵を使って署名を検証する
- クレーム(発行者、オーディエンス、有効期限)を検証する
- ペイロードからユーザー情報(
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つの重大な問題と、その解決方法を説明しています。
主な教訓:
- フロントエンドの型を書く前に、必ずバックエンドの Pydantic スキーマを読む
- オプションのフィールドには null チェックとデフォルト値を使用する
- 値が事前入力されたフォームには制御コンポーネントを使用する
- フロントエンドとバックエンドでフィールド名を正確に一致させる
- モックデータではなく、実際のバックエンドのレスポンスでテストする
詳しい解決策と予防策については、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 を使用してください。