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)
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
Templates
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