Internal Dev Docs

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_URL is not configured
  • Redis when WORKSPACE_AUTH_REDIS_URL is configured

In both modes:

  • the browser gets only an httpOnly session cookie containing a handle (mem:<uuid> or redis:<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_SECRET for session encryption (AUTH_SESSION_SECRET remains a legacy alias)
  • WORKSPACE_AUTH_REDIS_URL for Redis-backed session storage
  • WORKSPACE_AUTH_REDIS_KEY_PREFIX for optional Redis namespacing
  • KEYCLOAK_SSO_BASE_URL, KEYCLOAK_CLIENT_ID, and KEYCLOAK_SCOPE for Keycloak auth (SSO_* aliases remain migration-only)
  • PROFILE_SERVICE_URL for the profile-service seam (origin-only http/https URL), plus CRM_API_URL for the remaining CRM-backed workspace seams (FRM_API_URL remains 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 updatedAt persistence 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:

  • accessToken expires
  • refreshToken may 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:

  1. Without Redis in non-production

    • login works
    • /auth/callback produces a mem: workspace session handle
    • downstream token-backed calls can reuse the stored Keycloak token set
  2. 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

On this page