| 3 min read

JWT Authentication Patterns for AI-Powered APIs

JWT authentication FastAPI API security Supabase

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.