Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Auth (JWKS-cached)

Authentication verifies Clerk-issued RS256 JWTs against Clerk’s JWKS endpoint. The JWKS is cached with a 1-hour TTL, and an asyncio.Lock prevents a thundering herd from hammering Clerk’s endpoint the instant the cache expires. The check is double-checked inside the lock.

# backend/app/core/auth.py (excerpt)
_jwks_cache: dict | None = None
_jwks_cache_time: float = 0
_jwks_lock = asyncio.Lock()
JWKS_CACHE_TTL = 3600 # 1 hour
async def _get_jwks() -> dict:
"""Fetch and cache JWKS from Clerk with TTL-based expiration.
Uses asyncio.Lock to prevent thundering herd on cache expiry.
Uses a persistent httpx.AsyncClient to avoid per-request TLS.
"""
current_time = time.time()
if _jwks_cache is not None and (current_time - _jwks_cache_time) < JWKS_CACHE_TTL:
return _jwks_cache
async with _jwks_lock:
# Double-check after acquiring the lock
if _jwks_cache is not None and (time.time() - _jwks_cache_time) < JWKS_CACHE_TTL:
return _jwks_cache
response = await _http_client.get(settings.clerk_jwks_url)
response.raise_for_status()
_jwks_cache = response.json()
_jwks_cache_time = time.time()
return _jwks_cache

This is an excerpt — the shipped file also handles key rotation (falling back to a refetch when a token’s kid is not in the cached set) and the full RS256 verification. The behavior is covered by backend/tests/test_auth_cache.py, which asserts the cache serves from memory within the TTL and refetches after it expires.

The authenticated user is exposed to routes as a FastAPI dependency, so your endpoints just declare user: Annotated[User, Depends(get_current_user)] and stay clean.