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. 01通过 JWKS 验证 JWT
  2. 02配置 FastAPI 身份验证依赖
  3. 03校验用户标识与路由权限
  4. 04支持 JWT 自定义 UUID 声明
  5. 05提供验证脚本和集成模板

适用场景

  1. 01将 Better Auth 登录接入 FastAPI
  2. 02为 API 路由添加用户级访问控制
  3. 03排查 JWKS、令牌或 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

⚠️ 重要——需要两个独立的表:

  1. session 表必须包含 token 列(Better Auth 的核心要求)

    • 错误:column "token" of relation "session" does not exist
    • 修复方法:参见数据库架构问题
  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,但应用通常需要 UUID,以确保 API 路由和数据库外键中的类型一致。

解决方案: 采用混合 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)

从 JWT 自定义声明(而非 sub)中提取 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. 在 JWKS 中通过 kid 查找匹配的公钥
  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 hook 和 JWT 插件 — 参见 UUID 集成问题
"User not found after registration"双重身份验证系统冲突 — 参见 UUID 集成问题
"authClient.useSession is not a function"在 useEffect 中使用 authClient.getSession() — 参见 前端集成问题
"No authentication token available"使用 session.data.session.token,而不是 session.session.token — 参见 前端集成问题
"Unable to find matching signing key"在 jwt_verification.py 中清除 JWKS 缓存
"Token has expired"前端需要刷新会话
"Invalid token claims"检查签发者/受众是否与 BETTER_AUTH_URL 匹配
403 Forbidden(UUID 不匹配)确保比较的是 UUID,而不是 String 与 UUID — 参见 UUID 集成问题
前端-后端集成问题(新增 - 2026-01-02)
问题根本原因解决方案
尽管返回 200 OK,但任务未显示后端返回数组,前端期望分页对象使用 Array.isArray() 检查并处理两种格式 - 参见前端-后端集成
标签筛选导致崩溃后端返回标签对象 {id, name, color},前端期望 number[]更新 TypeScript 类型,使其与 Pydantic 架构匹配 - 参见标签筛选
分页显示“NaN”可选的优先级字段未进行空值检查便参与运算为可选字段添加空值检查和默认值 - 参见优先级排序
标签未保存到数据库TaskCreate 架构不接受 tags 字段使用多步操作:先创建任务,再分配标签 - 参见标签分配
编辑表单字段为空白非受控组件、字段名称不匹配以及日期时间格式问题使用受控组件,匹配字段名称,并转换日期时间 - 参见编辑表单
500 错误:时区比较比较不带时区偏移和带时区偏移的日期时间比较前将两者都规范化为 UTC - 参见时区修复
标签颜色验证失败前端要求颜色,后端允许颜色为可选项在 Zod 架构中将颜色设为可选,并提供默认值 - 参见标签颜色
标签筛选复选框失效后端返回 id: number,FilterContext 使用 string[]将 ID 转换为字符串以进行比较 - 参见标签筛选器

**📚 必读内容:**请参阅故障排除指南中的前端-后端集成问题部分,了解包含代码示例的详细修复方法。本部分记录了实现过程中发现的 8 个关键问题及其解决方法。

关键经验:

  1. 编写前端类型前,务必先阅读后端 Pydantic 架构
  2. 使用空值检查和默认值处理可选字段
  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。

评论

支持 Markdown