Keycloak token and session storage
Runtime notes for SSO Profile auth sessions, Redis storage, and downstream token reuse.
Auth Notes: Keycloak Tokens and Session Storage
Related internal docs:
TL;DR
Authenticated workspace sessions now always use a server-side session store.
There are two runtime modes:
- memory in non-production when
WORKSPACE_AUTH_REDIS_URLis not configured - Redis when
WORKSPACE_AUTH_REDIS_URLis configured
In both modes:
- the browser gets only an
httpOnlysession cookie containing a handle (mem:<uuid>orredis:<uuid>) - the encrypted session payload is stored server-side
- the stored payload can include the current Keycloak token set
- server-side CRM / ticketing calls can reuse and refresh tokens without exposing them to browser JavaScript
In production, Redis is required.
Canonical env names
Use these backend env names going forward:
WORKSPACE_AUTH_SESSION_SECRETfor session encryption (AUTH_SESSION_SECRETremains a legacy alias)WORKSPACE_AUTH_REDIS_URLfor Redis-backed session storageWORKSPACE_AUTH_REDIS_KEY_PREFIXfor optional Redis namespacingKEYCLOAK_SSO_BASE_URL,KEYCLOAK_CLIENT_ID, andKEYCLOAK_SCOPEfor Keycloak auth (SSO_*aliases remain migration-only)PROFILE_SERVICE_URLfor the profile-service seam (origin-onlyhttp/httpsURL), plusCRM_API_URLfor the remaining CRM-backed workspace seams (FRM_API_URLremains a legacy alias)
Session modes in code
Memory mode
Used automatically in non-production when Redis is not configured.
- The cookie stores a handle like
mem:<uuid>. - The encrypted session payload lives in server memory.
- The stored payload includes the reduced workspace session and the Keycloak token set.
- Sessions disappear when the dev server restarts.
This is the default local-dev path now, which means developers can still exercise downstream SSO-backed calls without running local Redis.
Redis mode
Used when WORKSPACE_AUTH_REDIS_URL is configured.
- The cookie stores a handle like
redis:<uuid>. - The encrypted session payload lives in Redis.
- The stored payload includes the reduced workspace session and the Keycloak token set.
- Concurrent refreshes use a lock plus compare-and-swap versioning so only one request wins the refresh/update race.
Production behavior
Production requires Redis-backed session storage.
If WORKSPACE_AUTH_REDIS_URL is missing in production, authenticated workspace sessions fail closed instead of silently downgrading to process-local memory.
What changed in this PR
src/features/auth/app-session.server.ts- reduced session storage to two real runtime modes: memory and Redis
- removed the old inline-cookie auth-session path
- removed unused
updatedAtpersistence metadata
src/features/auth/workspace-auth-redis.server.ts- kept Redis as the shared persistence layer
- trimmed adapter indirection and the stored session shape
src/features/auth/workspace-keycloak-token.server.ts- kept refresh lock / CAS coordination
- removed storage-mode threading that no longer added value
src/features/auth/keycloak.server.ts- moved Keycloak discovery/token/userinfo transport onto the shared upstream HTTP helper
src/lib/http-origin.server.ts- now reuses the shared cookie parser instead of keeping a second cookie scanner
Runtime behavior
Login and callback
/auth/callback always exchanges the authorization code for Keycloak tokens and user info.
It then creates a server-side workspace session:
- memory mode in local/non-production without Redis
- Redis mode when Redis is configured
So the callback still works locally without Redis, but it no longer relies on inline encrypted cookie sessions for authenticated workspace state.
Logout
/auth/logout reads the current workspace session, deletes any persisted server-side session if one exists, clears the cookie, and passes idTokenHint to Keycloak logout when available.
Auth/session debug
/auth/session?debug=1 reports whether a session exists, whether a Keycloak token set is present, and which storage mode is active. Optional flags include ticketing=1, crm=1, and profile=1 for downstream diagnostics.
Downstream server calls
Profile-service, CRM, and other server-side integrations that need the signed-in user's Keycloak token rely on resolveWorkspaceKeycloakAccessToken().
- In memory mode, the helper reads and refreshes the token set from the local process store.
- In Redis mode, it reads and refreshes the token set from Redis.
Why server-side storage is still required
The reduced workspace session is mostly stable for the life of a login.
The Keycloak token set is not:
accessTokenexpiresrefreshTokenmay rotate- concurrent requests can race while refreshing
That makes the token set shared mutable session state. Both memory mode and Redis mode give the server one place to keep the latest valid token set for the current environment. Redis is the production-safe shared version of that boundary.
Verification expectations
When validating auth behavior, check both paths:
-
Without Redis in non-production
- login works
/auth/callbackproduces amem:workspace session handle- downstream token-backed calls can reuse the stored Keycloak token set
-
With Redis configured
- login works
- the session cookie contains a
redis:handle - downstream calls can reuse stored tokens
- expired tokens refresh successfully
- logout deletes the persisted server-side session