JWT sessions and Redis
BearerTokenSession shape, Redis keys, access/refresh lifecycle, logout and refresh.
Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers
Learning outcomes by role
Stakeholders
- Explain why JWTs alone are insufficient for revocation and why Redis outages affect interactive auth.
Business analysts
- Document session invalidation scenarios (logout, password change) for user-facing acceptance criteria.
Solution architects
- Map Redis key layout and TTL strategy to HA Redis and backup policies.
Developers
- Trace BearerTokenSession, SessionStoreRepository, and AuthService for access and refresh flows.
Testers
- Predict 401 when Redis rows are deleted while JWTs still verify cryptographically.
After login, the server returns a short-lived JWT; the authoritative session (permissions, org memberships) lives in Redis, keyed by jti inside the token. Deleting Redis session rows yields 401 even when the JWT signature still verifies—follow BearerTokenSession, SessionStoreRepository, AuthService, and the auth HTTP routes for the full lifecycle.
Summary for stakeholders
Section titled “Summary for stakeholders”- Revocation — Session truth in Redis enables immediate logout and refresh rotation without waiting for JWT expiry.
- Operational coupling — Redis availability directly affects interactive sessions (see How the platform works).
Purpose
Section titled “Purpose”- Instant revocation — Deleting
session:{jti}invalidates the access token even if the JWT signature is still valid. - Rich session —
global_permissions,org_permissions,membership_org_ids, andorg_admin_idsare loaded at login/refresh and stored in Redis JSON.
Redis layout
Section titled “Redis layout”From the session store module docstring:
| Key pattern | Role |
|---|---|
session:{jti} | Serialized BearerTokenSession payload; TTL ≈ access token TTL |
user_sessions:{user_id} | Set of active access jti values (for bulk invalidation paths) |
refresh:{refresh_jti} | Refresh token metadata (user_id, access_jti, …) |
user_refresh_sessions:{user_id} | Set of active refresh jtis |
oauth_state:{state} | OAuth social login PKCE handoff |
oauth2_code:{code} | Short-lived OAuth2 authorization code payload |
TTL for access sessions defaults from global_settings.access_token_ttl_seconds (bootstrapped in lifespan if missing). Refresh TTL uses refresh_token_ttl_seconds.
Request path
Section titled “Request path”AuthenticationMiddlewarevalidates the JWT signature withJWTAuth.TenantContextMiddlewaredecodes the JWT again with the app secret to readjti, thensession_store.get_session(jti).- If Redis returns nothing,
request.state.sessionis null →require_sessionyields 401 on protected routes.
Issuing tokens
Section titled “Issuing tokens”POST /oauth2/token (password, refresh, authorization_code grants) is implemented in the OAuth2 stack; domain AuthService creates a BearerTokenSession via create_session, stores it in Redis, optionally creates a refresh row, and returns a JWT built with _build_jwt including the new jti.
Refresh flow
Section titled “Refresh flow”AuthService.refresh:
- Loads
refresh:{refresh_jti}— missing → 401 invalid refresh. - Recomputes permission fields via
_session_auth_fields(user_id). create_session(new accessjti),rotate_refresh_token(new refresh jti, old deleted).delete_session(old_access_jti)so the previous access JWT stops working.
Logout
Section titled “Logout”DELETE /api/auth/logout:
- Reads
request.state.token_jti(set by tenant middleware when Redis session existed). - Calls
AuthService.logout(jti, refresh_jti=...)—delete_session(jti)and optionaldelete_refresh_tokenifX-Refresh-Tokenheader is sent.
If token_jti is missing (edge cases), logout may no-op the Redis delete but still return 204.
Limitations
Section titled “Limitations”- Permissions for interactive users live in Redis with the session record; validating the JWT signature alone does not prove the session is still active.
- Redis unavailable — Session reads fail → callers typically see 401 until Redis is healthy and they sign in again.
- API keys — Use a separate session shape with no
session:{jti}row; see API keys.