Skip to content

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.

  • 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).
  • Instant revocation — Deleting session:{jti} invalidates the access token even if the JWT signature is still valid.
  • Rich sessionglobal_permissions, org_permissions, membership_org_ids, and org_admin_ids are loaded at login/refresh and stored in Redis JSON.

From the session store module docstring:

Key patternRole
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.

  1. AuthenticationMiddleware validates the JWT signature with JWTAuth.
  2. TenantContextMiddleware decodes the JWT again with the app secret to read jti, then session_store.get_session(jti).
  3. If Redis returns nothing, request.state.session is nullrequire_session yields 401 on protected routes.

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.

AuthService.refresh:

  1. Loads refresh:{refresh_jti} — missing → 401 invalid refresh.
  2. Recomputes permission fields via _session_auth_fields(user_id).
  3. create_session (new access jti), rotate_refresh_token (new refresh jti, old deleted).
  4. delete_session(old_access_jti) so the previous access JWT stops working.

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 optional delete_refresh_token if X-Refresh-Token header is sent.

If token_jti is missing (edge cases), logout may no-op the Redis delete but still return 204.

  • 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.