JWT Authentication Patterns for AI-Powered APIs
Why JWT for AI APIs
AI APIs need authentication that is stateless, fast, and compatible with multiple client types. JWTs (JSON Web Tokens) tick all these boxes. They encode user identity and permissions directly in the token, eliminating the need for database lookups on every request. For AI APIs where response latency already includes LLM processing time, minimizing auth overhead matters.
I use Supabase Auth to issue JWTs and FastAPI middleware to validate them. This combination gives me robust authentication with minimal code.
How JWT Auth Works in My Stack
The flow is straightforward:
- User authenticates with Supabase (email/password, OAuth, or magic link)
- Supabase issues a JWT containing the user ID, email, and role
- Client includes the JWT in the Authorization header of API requests
- FastAPI middleware validates the JWT and extracts user information
- The request proceeds with the authenticated user context
FastAPI JWT Middleware
Here is my production JWT validation middleware:
from fastapi import Request, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime
security = HTTPBearer()
SUPABASE_JWT_SECRET = os.environ['SUPABASE_JWT_SECRET']
class AuthenticatedUser:
def __init__(self, user_id: str, email: str, role: str):
self.user_id = user_id
self.email = email
self.role = role
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> AuthenticatedUser:
token = credentials.credentials
try:
payload = jwt.decode(
token,
SUPABASE_JWT_SECRET,
algorithms=["HS256"],
audience="authenticated"
)
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
return AuthenticatedUser(
user_id=payload["sub"],
email=payload.get("email", ""),
role=payload.get("role", "user")
)
Protecting API Endpoints
With the dependency defined, protecting any endpoint is a single parameter addition:
@app.post("/api/analyze")
async def analyze_document(
request: AnalyzeRequest,
user: AuthenticatedUser = Depends(get_current_user)
):
# user.user_id is guaranteed to be valid here
result = await pipeline.run(request.content, user_id=user.user_id)
return result
@app.get("/api/history")
async def get_history(
user: AuthenticatedUser = Depends(get_current_user)
):
# Only returns this user's history (plus RLS enforces this at DB level)
results = await supabase.table('analysis_results') \
.select('*') \
.eq('user_id', user.user_id) \
.order('created_at', desc=True) \
.limit(50) \
.execute()
return results.data
Role-Based Access Control
Different users need different levels of access. I implement RBAC with a simple dependency that checks the user's role:
def require_role(allowed_roles: list[str]):
async def role_checker(
user: AuthenticatedUser = Depends(get_current_user)
) -> AuthenticatedUser:
if user.role not in allowed_roles:
raise HTTPException(
status_code=403,
detail=f"Role '{user.role}' not authorized for this endpoint"
)
return user
return role_checker
# Only admins can access system configuration
@app.put("/api/config")
async def update_config(
config: ConfigUpdate,
user: AuthenticatedUser = Depends(require_role(["admin"]))
):
await save_config(config)
return {"status": "updated"}
# Both admins and premium users can access advanced features
@app.post("/api/advanced-analyze")
async def advanced_analyze(
request: AnalyzeRequest,
user: AuthenticatedUser = Depends(require_role(["admin", "premium"]))
):
result = await advanced_pipeline.run(request.content)
return result
API Key Authentication for Server-to-Server
Not all API consumers are end users with Supabase accounts. For server-to-server communication, I support API key authentication alongside JWT:
from fastapi.security import APIKeyHeader
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
async def get_api_user(
api_key: str = Depends(api_key_header),
credentials: HTTPAuthorizationCredentials = Depends(
HTTPBearer(auto_error=False)
)
) -> AuthenticatedUser:
# Try API key first
if api_key:
key_record = await lookup_api_key(api_key)
if key_record:
return AuthenticatedUser(
user_id=key_record['user_id'],
email=key_record['email'],
role=key_record['role']
)
# Fall back to JWT
if credentials:
return await get_current_user(credentials)
raise HTTPException(status_code=401, detail="Authentication required")
Rate Limiting by Authentication Level
Different authentication levels get different rate limits. Free users get 100 requests per hour, premium users get 1,000, and API key users get custom limits based on their plan:
from collections import defaultdict
from time import time
RATE_LIMITS = {
"free": 100,
"premium": 1000,
"admin": 10000
}
request_counts = defaultdict(list)
async def check_rate_limit(user: AuthenticatedUser):
now = time()
window = 3600 # 1 hour
# Clean old entries
request_counts[user.user_id] = [
t for t in request_counts[user.user_id]
if now - t < window
]
limit = RATE_LIMITS.get(user.role, 100)
if len(request_counts[user.user_id]) >= limit:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded. Limit: {limit}/hour"
)
request_counts[user.user_id].append(now)
Security Best Practices
- Always validate the audience claim: Prevents tokens issued for other services from being used on your API
- Check expiration: Never accept expired tokens, even if the signature is valid
- Use HTTPS exclusively: JWTs in plain HTTP can be intercepted and replayed
- Rotate secrets: Plan for JWT secret rotation from the beginning
- Log auth failures: Monitor for brute force attempts and invalid token patterns
Authentication is not a feature you add later. It is a foundation you build on from the start. Getting it right early prevents painful retrofitting and security incidents.
Getting Started
If you are building an AI API with FastAPI and Supabase, you can have JWT authentication working in under an hour. Start with the basic middleware shown above, add it to your most sensitive endpoints, and expand from there. The combination of Supabase Auth issuing tokens and FastAPI validating them is clean, secure, and requires very little ongoing maintenance.