betterauth-fastapi-jwt-bridge
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBetter Auth + FastAPI JWT Bridge
Better Auth + FastAPI JWT 认证桥接
Implement production-ready JWT authentication between Better Auth (Next.js) and FastAPI using JWKS verification for secure, stateless authentication.
使用JWKS验证在Better Auth(Next.js)与FastAPI之间实现可用于生产环境的JWT认证,以实现安全的无状态认证。
Architecture
架构
User Login (Frontend)
↓
Better Auth → Issues JWT Token
↓
Frontend API Request → Authorization: Bearer <token>
↓
FastAPI Backend → Verifies JWT with JWKS → Returns filtered data用户登录(前端)
↓
Better Auth → 签发JWT令牌
↓
前端API请求 → Authorization: Bearer <token>
↓
FastAPI后端 → 使用JWKS验证JWT → 返回过滤后的数据Quick Start Workflow
快速开始流程
Step 1: Enable JWT in Better Auth (Frontend)
步骤1:在Better Auth(前端)中启用JWT
typescript
// 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
})Database Migration Required:
After adding JWT plugin, run migrations to create required tables:
bash
undefinedtypescript
// lib/auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [jwt()], // 启用JWT + JWKS端点
// ... 其他配置
})需要执行数据库迁移:
添加JWT插件后,运行迁移以创建所需表:
bash
undefinedNext.js (Better Auth CLI)
Next.js(Better Auth CLI)
npx @better-auth/cli migrate
**⚠️ IMPORTANT - Two Separate Tables Required:**
1. **`session` table must have `token` column** (core Better Auth requirement)
- Error: `column "token" of relation "session" does not exist`
- Fix: See [Database Schema Issues](references/troubleshooting.md#database-schema-issues)
2. **`jwks` table must exist** (JWT plugin requirement)
- Error: `relation "jwks" does not exist`
- Fix: See [Database Schema Issues](references/troubleshooting.md#database-schema-issues)
These are **separate migrations**. The JWT plugin creates the `jwks` table but does NOT modify the `session` table.npx @better-auth/cli migrate
**⚠️ 重要 - 需要两张独立的表:**
1. **`session`表必须包含`token`列**(Better Auth核心要求)
- 错误:`column "token" of relation "session" does not exist`
- 修复方案:查看[数据库架构问题](references/troubleshooting.md#database-schema-issues)
2. **必须存在`jwks`表**(JWT插件要求)
- 错误:`relation "jwks" does not exist`
- 修复方案:查看[数据库架构问题](references/troubleshooting.md#database-schema-issues)
这是**两次独立的迁移**。JWT插件会创建`jwks`表,但不会修改`session`表。Step 2: Verify JWKS Endpoint
步骤2:验证JWKS端点
Test the JWKS endpoint is working:
bash
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks测试JWKS端点是否正常工作:
bash
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwksStep 3: Implement Backend Verification
步骤3:实现后端验证
Copy templates from to your FastAPI project:
assets/- →
assets/jwt_verification.pybackend/app/auth/jwt_verification.py - →
assets/auth_dependencies.pybackend/app/auth/dependencies.py
Install dependencies:
bash
pip install fastapi python-jose[cryptography] pyjwt cryptography httpx将中的模板复制到你的FastAPI项目中:
assets/- →
assets/jwt_verification.pybackend/app/auth/jwt_verification.py - →
assets/auth_dependencies.pybackend/app/auth/dependencies.py
安装依赖:
bash
pip install fastapi python-jose[cryptography] pyjwt cryptography httpxStep 4: Protect API Routes
步骤4:保护API路由
python
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)python
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与认证用户匹配
return get_user_tasks(user_id)Step 5: Configure Frontend API Client
步骤5:配置前端API客户端
Copy to and use:
assets/api_client.tsfrontend/lib/api-client.tstypescript
import { getTasks, createTask } from "@/lib/api-client"
const tasks = await getTasks(userId)⚠️ React Component Pattern:
Better Auth does NOT provide a hook. Use with :
useSession()authClient.getSession()useEffecttypescript
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>
}See Frontend Integration Issues for complete examples.
将复制到并使用:
assets/api_client.tsfrontend/lib/api-client.tstypescript
import { getTasks, createTask } from "@/lib/api-client"
const tasks = await getTasks(userId)⚠️ React组件模式:
Better Auth未提供钩子。请结合使用:
useSession()useEffectauthClient.getSession()typescript
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>欢迎 {user?.name}</div>
}完整示例请查看前端集成问题。
Better Auth UUID Integration (Hybrid ID Architecture)
Better Auth UUID集成(混合ID架构)
Problem Solved: Better Auth uses String IDs internally, but applications often need UUID for type consistency across API routes and database foreign keys.
Solution: Hybrid ID approach - User table has both (String, Better Auth requirement) and (UUID, application use).
iduuid解决的问题: Better Auth内部使用字符串ID,但应用通常需要UUID以确保API路由和数据库外键的类型一致性。
解决方案: 混合ID方案 - 用户表同时包含(字符串,Better Auth要求)和(UUID,应用使用)。
iduuidDatabase Schema
数据库架构
sql
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,
...
);sql
CREATE TABLE "user" (
id VARCHAR PRIMARY KEY, -- Better Auth字符串ID
uuid UUID UNIQUE NOT NULL, -- 应用UUID ⭐
email VARCHAR UNIQUE NOT NULL,
"emailVerified" BOOLEAN DEFAULT FALSE,
name VARCHAR,
"createdAt" TIMESTAMP NOT NULL,
"updatedAt" TIMESTAMP NOT NULL
);
-- 由数据库自动生成UUID
ALTER TABLE "user" ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
-- 所有外键指向user.uuid
CREATE TABLE tasks (
id UUID PRIMARY KEY,
user_id UUID REFERENCES "user"(uuid) ON DELETE CASCADE, -- ⭐ 外键指向uuid
title VARCHAR NOT NULL,
...
);Frontend Configuration (Better Auth)
前端配置(Better Auth)
Add UUID generation hook and JWT custom claim:
typescript
// 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
}
},
}),
],
})添加UUID生成钩子和JWT自定义声明:
typescript
// 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,
// 用于获取数据库生成的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 }
}
}
},
// 在JWT负载中包含UUID
plugins: [
jwt({
algorithm: "EdDSA",
async jwt(user, session) {
return {
uuid: user.uuid, // ⭐ 供后端使用的自定义声明
}
},
}),
],
})Backend Pattern (FastAPI)
后端模式(FastAPI)
Extract UUID from JWT custom claim (not ):
subpython
undefined从JWT自定义声明中提取UUID(而非):
subpython
undefinedbackend/app/auth/dependencies.py
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
**Key Pattern:** Always query by `User.uuid` and validate against UUID from JWT custom claim.from uuid import UUID
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
payload = verify_jwt_token(token)
# 从自定义声明中提取UUID(而非'sub')
user_uuid_str = payload.get("uuid") # ⭐
user_uuid = UUID(user_uuid_str)
# 通过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, # 来自URL路径
current_user: User = Depends(get_current_user)
) -> User:
# 比较UUID(而非字符串ID)
if current_user.uuid != user_id:
raise HTTPException(403, "未授权")
return current_user
**核心模式:** 始终通过`User.uuid`查询用户,并与JWT自定义声明中的UUID进行验证。Key Components
核心组件
1. JWKS Verification Flow
1. JWKS验证流程
- Fetch JWKS (cached) from Better Auth endpoint
- Extract kid (key ID) from JWT token header
- Find matching public key in JWKS by kid
- Verify signature using Ed25519 public key
- Validate claims (issuer, audience, expiration)
- Extract user info from payload (claim)
sub
- 获取JWKS(已缓存)从Better Auth端点
- 提取kid(密钥ID)从JWT令牌头
- 在JWKS中查找匹配的公钥通过kid
- 使用Ed25519公钥验证签名
- 验证声明(签发者、受众、过期时间)
- 从负载中提取用户信息(声明)
sub
2. User Isolation Pattern
2. 用户隔离模式
Always verify from JWT matches in URL:
user_iduser_idpython
if current_user["user_id"] != user_id:
raise HTTPException(status_code=403, detail="Not authorized")This prevents users from accessing other users' data.
始终验证JWT中的与URL中的是否匹配:
user_iduser_idpython
if current_user["user_id"] != user_id:
raise HTTPException(status_code=403, detail="未授权")这可防止用户访问其他用户的数据。
3. JWT Payload Structure (Updated with UUID Integration)
3. JWT负载结构(集成UUID后更新)
json
{
"sub": "user_abc123", // Better Auth String ID
"uuid": "a1b2c3d4-e5f6...", // Application UUID (custom claim) ⭐
"email": "user@example.com",
"name": "User Name",
"iat": 1234567890, // Issued at
"exp": 1234567890, // Expiration
"iss": "http://localhost:3000",
"aud": "http://localhost:3000"
}Important: The custom claim is used for backend user identification and database queries.
Better Auth manages users with String IDs (), while the application uses UUIDs () for type consistency.
uuidsubuuidjson
{
"sub": "user_abc123", // Better Auth字符串ID
"uuid": "a1b2c3d4-e5f6...", // 应用UUID(自定义声明)⭐
"email": "user@example.com",
"name": "User Name",
"iat": 1234567890, // 签发时间
"exp": 1234567890, // 过期时间
"iss": "http://localhost:3000",
"aud": "http://localhost:3000"
}重要提示: 自定义声明用于后端用户识别和数据库查询。Better Auth使用字符串ID()管理用户,而应用使用UUID()以确保类型一致性。
uuidsubuuidEnvironment Configuration
环境配置
Frontend (.env.local):
bash
BETTER_AUTH_SECRET="min-32-chars-secret"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:8000"Backend (.env):
bash
BETTER_AUTH_URL="http://localhost:3000"
DATABASE_URL="postgresql://..."前端(.env.local):
bash
BETTER_AUTH_SECRET="min-32-chars-secret"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_API_URL="http://localhost:8000"后端(.env):
bash
BETTER_AUTH_URL="http://localhost:3000"
DATABASE_URL="postgresql://..."Testing & Validation
测试与验证
Test JWKS Endpoint
测试JWKS端点
bash
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwksExpected output shows public keys with , , , and fields.
kidktycrvxbash
python scripts/verify_jwks.py http://localhost:3000/api/auth/jwks预期输出应显示包含、、和字段的公钥。
kidktycrvxTest JWT Verification
测试JWT验证
bash
python scripts/test_jwt_verification.py \
--jwks-url http://localhost:3000/api/auth/jwks \
--token "eyJhbGci..."bash
python scripts/test_jwt_verification.py \
--jwks-url http://localhost:3000/api/auth/jwks \
--token "eyJhbGci..."Troubleshooting
故障排查
Authentication Issues
认证问题
| Issue | Solution |
|---|---|
| "relation 'jwks' does not exist" | Create JWKS table migration - see Database Schema Issues |
| "column 'token' does not exist" | Add |
| "Token missing UUID (uuid claim)" | Configure Better Auth hook and JWT plugin - see UUID Integration Issues |
| "User not found after registration" | Dual auth system conflict - see UUID Integration Issues |
| "authClient.useSession is not a function" | Use |
| "No authentication token available" | Use |
| "Unable to find matching signing key" | Clear JWKS cache in jwt_verification.py |
| "Token has expired" | Frontend needs to refresh session |
| "Invalid token claims" | Check issuer/audience match BETTER_AUTH_URL |
| 403 Forbidden (UUID mismatch) | Ensure UUID comparison, not String vs UUID - see UUID Integration Issues |
| 问题 | 解决方案 |
|---|---|
| "relation 'jwks' does not exist" | 创建JWKS表迁移 - 查看数据库架构问题 |
| "column 'token' does not exist" | 为session表添加 |
| "Token missing UUID (uuid claim)" | 配置Better Auth钩子和JWT插件 - 查看UUID集成问题 |
| "User not found after registration" | 双重认证系统冲突 - 查看UUID集成问题 |
| "authClient.useSession is not a function" | 在useEffect中使用 |
| "No authentication token available" | 使用 |
| "Unable to find matching signing key" | 清除jwt_verification.py中的JWKS缓存 |
| "Token has expired" | 前端需要刷新会话 |
| "Invalid token claims" | 检查签发者/受众是否与BETTER_AUTH_URL匹配 |
| 403 Forbidden(UUID不匹配) | 确保进行UUID比较,而非字符串与UUID比较 - 查看UUID集成问题 |
Frontend-Backend Integration Issues (NEW - 2026-01-02)
前端-后端集成问题(新增 - 2026-01-02)
| Issue | Root Cause | Solution |
|---|---|---|
| Tasks not displaying despite 200 OK | Backend returns array, frontend expects paginated object | Handle both formats with |
| Tag filtering crashes | Backend returns tag objects | Update TypeScript types to match Pydantic schemas - see Tag Filtering |
| Pagination shows "NaN" | Optional priority field used in arithmetic without null check | Add null checks with defaults for optional fields - see Priority Sorting |
| Tags not saving to database | TaskCreate schema doesn't accept tags field | Use multi-step operation: create task, then assign tags - see Tag Assignment |
| Edit form fields blank | Uncontrolled components + field name mismatches + datetime format | Use controlled components, match field names, convert datetime - see Edit Form |
| 500 Error: timezone comparison | Comparing offset-naive and offset-aware datetimes | Normalize both to UTC before comparison - see Timezone Fix |
| Tag color validation fails | Frontend required color, backend allows optional | Make color optional in Zod schema, provide defaults - see Tag Color |
| Tag filter checkboxes broken | Backend returns | Convert IDs to strings for comparison - see Tag Filters |
📚 Critical Reading: See Frontend-Backend Integration Issues section in troubleshooting guide for detailed fixes with code examples. This section documents 8 critical issues discovered during implementation and their resolutions.
Key Learnings:
- Always read backend Pydantic schemas before writing frontend types
- Handle optional fields with null checks and defaults
- Use controlled components for pre-filled forms
- Match field names exactly between frontend and backend
- Test with actual backend responses, not mocked data
See for detailed solutions and prevention strategies.
references/troubleshooting.md| 问题 | 根本原因 | 解决方案 |
|---|---|---|
| 返回200 OK但任务未显示 | 后端返回数组,前端期望分页对象 | 使用 |
| 标签筛选功能崩溃 | 后端返回标签对象 | 更新TypeScript类型以匹配Pydantic架构 - 查看标签筛选 |
| 分页显示"NaN" | 可选优先级字段在运算时未做空值检查 | 为可选字段添加空值检查和默认值 - 查看优先级排序 |
| 标签未保存到数据库 | TaskCreate架构不接受tags字段 | 使用多步操作:先创建任务,再分配标签 - 查看标签分配 |
| 编辑表单字段为空 | 非受控组件+字段名称不匹配+日期时间格式问题 | 使用受控组件,匹配字段名称,转换日期时间格式 - 查看编辑表单 |
| 500错误:时区比较 | 比较无时区偏移和有时区偏移的日期时间 | 在比较前将两者统一为UTC时间 - 查看时区修复 |
| 标签颜色验证失败 | 前端要求必填颜色,后端允许可选 | 在Zod架构中将颜色设为可选,提供默认值 - 查看标签颜色 |
| 标签筛选复选框失效 | 后端返回 | 将ID转换为字符串后再进行比较 - 查看标签筛选 |
📚 必读内容: 查看故障排查指南中的前端-后端集成问题章节,获取带代码示例的详细修复方案。该章节记录了实现过程中发现的8个关键问题及其解决方法。
核心经验:
- 在编写前端类型前务必阅读后端Pydantic架构
- 为可选字段添加空值检查和默认值
- 使用受控组件实现预填充表单
- 确保前端与后端的字段名称完全匹配
- 使用实际后端响应进行测试,而非模拟数据
详细解决方案和预防策略请查看。
references/troubleshooting.mdAdvanced Topics
高级主题
JWKS Caching Strategy
JWKS缓存策略
The implementation uses to cache JWKS responses:
@lru_cache- Cache invalidated if token has unknown
kid - Public keys rarely change (safe to cache)
- Reduces network calls to Better Auth
See for implementation details.
references/jwks-approach.md本实现使用缓存JWKS响应:
@lru_cache- 如果令牌包含未知则失效缓存
kid - 公钥极少变更(可安全缓存)
- 减少对Better Auth的网络请求
实现细节请查看。
references/jwks-approach.mdSecurity Checklist
安全检查清单
Before production:
- ✅ HTTPS only for all API calls
- ✅ Token expiration validated
- ✅ Issuer/audience claims verified
- ✅ User ID authorization enforced
- ✅ CORS properly configured
- ✅ Error messages don't leak sensitive info
See for complete list.
references/security-checklist.md上线前需完成:
- ✅ 所有API调用仅使用HTTPS
- ✅ 已验证令牌过期时间
- ✅ 已验证签发者/受众声明
- ✅ 已强制实施用户ID授权
- ✅ 已正确配置CORS
- ✅ 错误信息未泄露敏感信息
完整清单请查看。
references/security-checklist.mdResources
资源
scripts/
scripts/
- - Test JWKS endpoint availability
verify_jwks.py - - Validate JWT token verification
test_jwt_verification.py
- - 测试JWKS端点可用性
verify_jwks.py - - 验证JWT令牌验证功能
test_jwt_verification.py
references/
references/
- - Detailed JWKS implementation guide
jwks-approach.md - - Production security requirements
security-checklist.md - - Common issues and fixes
troubleshooting.md
- - JWKS实现详细指南
jwks-approach.md - - 生产环境安全要求
security-checklist.md - - 常见问题与修复方案
troubleshooting.md
assets/
assets/
- - Complete JWKS verification module template
jwt_verification.py - - FastAPI dependencies template
auth_dependencies.py - - Frontend API client template
api_client.ts - - Alembic migration templates for Better Auth tables (including token column fix)
better_auth_migrations.py
- - 完整的JWKS验证模块模板
jwt_verification.py - - FastAPI依赖项模板
auth_dependencies.py - - 前端API客户端模板
api_client.ts - - 用于Better Auth表的Alembic迁移模板(包含token列修复)
better_auth_migrations.py
Why JWKS Over Shared Secret?
为何选择JWKS而非共享密钥?
| Aspect | JWKS | Shared Secret |
|---|---|---|
| Security | ✅ Asymmetric (more secure) | ⚠️ Symmetric (less secure) |
| Scalability | ✅ Multiple backends | ⚠️ Secret must be shared |
| Production | ✅ Recommended | ⚠️ Development only |
| Complexity | Medium | Simple |
Recommendation: Always use JWKS for production.
| 方面 | JWKS | 共享密钥 |
|---|---|---|
| 安全性 | ✅ 非对称(更安全) | ⚠️ 对称(安全性较低) |
| 可扩展性 | ✅ 支持多个后端 | ⚠️ 必须共享密钥 |
| 生产环境适用性 | ✅ 推荐使用 | ⚠️ 仅适用于开发环境 |
| 复杂度 | 中等 | 简单 |
建议: 生产环境中始终使用JWKS。