Auth & JWT Lifecycle
When a user logs in, how do tokens get issued, refreshed, and revoked — and what's the real contract between FuzzyCode and every sibling service?
§1 The shape
FuzzyCode is the auth broker. Every sibling service trusts tokens issued by FuzzyCode. Supabase sits outside the platform as the identity provider; FuzzyCode mediates between it and the browser. Sibling services never talk to Supabase directly — they verify tokens against FuzzyCode's JWKS.
(broker) participant SB as Supabase
auth.users participant R as Redis
(refresh lock+cache) participant S as Sibling service
(Pages / Images / …) rect rgb(237, 217, 192) Note over U,SB: LOGIN — broker mediates between browser and Supabase U->>+F: POST /auth/login
email · password F->>+SB: password grant SB-->>-F: { access_token (JWT), refresh_token } F->>F: set_broker_auth_cookies()
7+ HttpOnly cookies · see §3 F-->>-U: 200 · Set-Cookie (×N) end rect rgb(230, 169, 94) Note over U,S: NORMAL REQUEST — sibling verifies against broker JWKS U->>+S: GET /some/route
Cookie: jwt_token · fc_active_ctx S->>+F: GET /auth/active-context/.well-known/jwks.json F-->>-S: { keys: [RSA JWK] } · (empty array if AC flag off) S->>S: verify JWT via JWKS-first (RS256)
fall back to HS256 secret S-->>-U: 200 · response end rect rgb(199, 217, 232) Note over U,R: REFRESH — Redis lock + cache prevents thundering herd U->>+F: POST /auth/session/refresh F->>+R: SETNX refresh_lock_key (ex 10s) alt lock acquired F->>+SB: refresh_token grant SB-->>-F: new access_token + refresh_token F->>R: SET refresh_result_key (ex 5s) F->>F: set_broker_auth_cookies() F-->>U: 200 · new Set-Cookie else lock contended F->>R: poll refresh_result_key (≤ 1.6s) alt cached result arrived F-->>U: 200 · reuses sibling's fresh tokens else still missing F-->>-U: 409 REFRESH_IN_PROGRESS end end deactivate R end rect rgb(253, 236, 234) Note over U,F: REVOCATION — realtime projection invalidates sessions F->>F: runtime_controls_status subscribes
to child/parent projections Note over F: on high_watermark bump,
actor hydration fails (blocked)
→ subsequent requests 401 end
§2 JWT verification chain (at every sibling service)
Every sibling service runs this exact ladder. Order matters: JWKS-first favors asymmetric keys (rotatable); HS256 fallback keeps legacy secrets working during rotation. Expired tokens are sometimes allowed on public routes (allow_expired=True) but always rejected on protected routes.
Cookie: jwt_token] --> B[Fetch JWKS
from FuzzyCode] B --> C{Token kid
present in JWKS?} C -->|yes| D[Verify asymmetric
RS256 / ES256 / EdDSA] C -->|no| E[HS256 fallback
MY_SUPABASE_JWT_SECRET
or SUPABASE_JWT_SECRET_KEY] D -->|valid| F[Extract claims
sub · role · exp · session_version] E -->|valid| F D -->|invalid| X[401 Unauthorized] E -->|invalid| X F --> G{Protected route?} G -->|yes, and exp < now| X G -->|no, or exp > now| H[Check active-context token
if present and AC_ENABLED] H -->|valid AC + matches sub| I[Authorized] H -->|AC invalid or mismatch| X H -->|AC absent and AC_ENABLED=false| I class A,B,D,E,F,H step class I ok class X fail
§3 Cookie inventory
Twelve cookies in play at login. Grouped by role; TTLs are from code.
WIP_DOCS/auth-context-analysis.md describes the system as user_id/user_name + jwt_token validated by @jwt_required. That document is retired — it describes pre-broker auth. The real system is the broker cookie set above plus server-side VerifiedActor hydration.
§4 Environment state
UAT · 2026-04-17
Active-context enabled. fc_active_ctx cookie minted at login + refresh. JWKS endpoint returns RSA key. Sibling services enforce active-context claims (e.g., sessions gated on session_version, child-scoped permissions). Entitlements flow from the active-context token rather than raw JWT claims.
PROD · 2026-04-17
Active-context disabled across every service .replit (ACTIVE_CONTEXT_ENABLED="false"). JWKS returns { keys: [] }. Sibling services gate on raw JWT claims — meaning child-direct sessions without SUBSCRIPTION:ACTIVE / CREATE_IMAGE:FULL cannot use media services (Images/Sounds/Sprites). The active-context path exists and is live in UAT; prod cutover is planned but not yet in progress.
§5 Refresh contention — what happens when 5 tabs all wake up
Only one refresh can hold the Supabase refresh-token at a time (Supabase rotates refresh tokens on each use). The broker serializes via Redis and shares the result, so concurrent tabs never burn each other's refresh.
Evidence: auth.py:4160-4321 (handler), broker.py:374-381 (key helpers). Client-side freshness supervisor is broker_session_freshness_model.js:8-50 — it throttles refresh requests from a single tab.
§6 Verification pointers
- Broker cookie set
FuzzyCode/quart_server/auth/broker.py:1419-1515(set_broker_auth_cookies) - All cookie names
broker.py:51-83 - Refresh endpoint
FuzzyCode/quart_server/blueprints/auth.py:4124-4321 - Redis refresh lock/cache keys
broker.py:374-381 - JWT verification ladder
FuzzyCode/quart_server/auth/supabase_jwt_verifier.py:36-73 - JWKS endpoint (broker)
FuzzyCode/quart_server/blueprints/auth.py:3193-3197 - Active-context cookie mint
active_context.py:150-200+ gated byactive_context.py:26-32 - Sibling verifier (Pages)
FuzzycodePagesFlaskServer/active_context_verifier.py:99-143 - Sibling verifier (ImageBuddy)
ImageBuddyRobustFastAPI/active_context_verifier.py(python-jose variant with JWKS cache) - Parent-child cookie (HS256 signed)
broker.py:574-606, 795-826 - Session-version / cutoff
broker.py:725-736 - Runtime-controls realtime + reconcile
FuzzyCode/quart_server/services/runtime_controls_status.py:40-50 - Revocation projection migration
supabase/migrations_after_baseline/20260416190000_add_runtime_control_projections.sql - Runtime probe (canary)
scripts/run_auth_canary.py - JWT matrix check
scripts/uat_jwt_matrix_check.py - Prod active-context flag
grep ACTIVE_CONTEXT_ENABLED */.replit— all"false"