Better Auth Python Integration Skill
Integrate Python/FastAPI backends with Better Auth (TypeScript) authentication server using JWT verification.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Next.js App │────▶│ Better Auth │────▶│ PostgreSQL │ │ (Frontend) │ │ (Auth Server) │ │ (Database) │ └────────┬────────┘ └────────┬────────┘ └─────────────────┘ │ │ │ JWT Token │ JWKS Endpoint ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ FastAPI Backend │ │ (Verifies JWT tokens) │ └─────────────────────────────────────────────────────────────────┘
Quick Start
Installation
pip
pip install fastapi uvicorn pyjwt cryptography httpx
poetry
poetry add fastapi uvicorn pyjwt cryptography httpx
uv
uv add fastapi uvicorn pyjwt cryptography httpx
Environment Variables
DATABASE_URL=postgresql://user:password@localhost:5432/mydb BETTER_AUTH_URL=http://localhost:3000
ORM Integration (Choose One)
ORM Guide
SQLModel reference/sqlmodel.md
SQLAlchemy reference/sqlalchemy.md
Basic JWT Verification
app/auth.py
import os import httpx import jwt from dataclasses import dataclass from typing import Optional from fastapi import HTTPException, Header, status
BETTER_AUTH_URL = os.getenv("BETTER_AUTH_URL", "http://localhost:3000")
@dataclass class User: id: str email: str name: Optional[str] = None
_jwks_cache: dict = {}
async def get_jwks() -> dict: global _jwks_cache if not _jwks_cache: async with httpx.AsyncClient() as client: response = await client.get(f"{BETTER_AUTH_URL}/.well-known/jwks.json") response.raise_for_status() _jwks_cache = response.json() return _jwks_cache
async def verify_token(token: str) -> User: if token.startswith("Bearer "): token = token[7:]
jwks = await get_jwks()
public_keys = {}
for key in jwks.get("keys", []):
public_keys[key["kid"]] = jwt.algorithms.RSAAlgorithm.from_jwk(key)
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
if not kid or kid not in public_keys:
raise HTTPException(status_code=401, detail="Invalid token key")
payload = jwt.decode(token, public_keys[kid], algorithms=["RS256"])
return User(
id=payload.get("sub"),
email=payload.get("email"),
name=payload.get("name"),
)
async def get_current_user( authorization: str = Header(..., alias="Authorization") ) -> User: return await verify_token(authorization)
Protected Route
from fastapi import Depends from app.auth import User, get_current_user
@app.get("/api/me") async def get_me(user: User = Depends(get_current_user)): return {"id": user.id, "email": user.email, "name": user.name}
Examples
Pattern Guide
Protected Routes examples/protected-routes.md
JWT Verification examples/jwt-verification.md
Templates
Template Purpose
templates/auth.py JWT verification module
templates/main.py FastAPI app template
templates/database_sqlmodel.py SQLModel database setup
templates/models_sqlmodel.py SQLModel models
Quick SQLModel Example
from sqlmodel import SQLModel, Field, Session, select from typing import Optional from datetime import datetime
class Task(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) title: str = Field(index=True) completed: bool = Field(default=False) user_id: str = Field(index=True) # From JWT 'sub' claim
@app.get("/api/tasks") async def get_tasks( user: User = Depends(get_current_user), session: Session = Depends(get_session), ): statement = select(Task).where(Task.user_id == user.id) return session.exec(statement).all()
Frontend Integration
Getting JWT from Better Auth
import { authClient } from "./auth-client";
const { data } = await authClient.token(); const jwtToken = data?.token;
Sending to FastAPI
async function fetchAPI(endpoint: string) { const { data } = await authClient.token();
return fetch(${API_URL}${endpoint}, {
headers: {
Authorization: Bearer ${data?.token},
"Content-Type": "application/json",
},
});
}
Security Considerations
-
Always use HTTPS in production
-
Validate issuer and audience to prevent token substitution
-
Handle token expiration gracefully
-
Refresh JWKS when encountering unknown key IDs
-
Don't log tokens - they contain sensitive data
Troubleshooting
JWKS fetch fails
-
Ensure Better Auth server is running
-
Check JWKS endpoint is accessible
-
Verify network connectivity
Token validation fails
-
Check issuer/audience match exactly
-
Verify token hasn't expired
-
Check algorithm compatibility (RS256)
CORS errors
-
Configure CORS middleware properly
-
Allow credentials if using cookies
-
Check origin is in allowed list