Skip to content

fix(auth): align cookie lifetimes with session and refresh token boundaries#3235

Open
ralf157 wants to merge 7 commits intotemporalio:mainfrom
ralf157:main
Open

fix(auth): align cookie lifetimes with session and refresh token boundaries#3235
ralf157 wants to merge 7 commits intotemporalio:mainfrom
ralf157:main

Conversation

@ralf157
Copy link

@ralf157 ralf157 commented Mar 19, 2026

Description & motivation 💭

Fixes two bugs in auth session and cookie handling:

1. maxSessionDuration not configurable in Docker deployments (#3223)
The field existed in the config struct but was missing from docker.yaml, making it impossible to enforce session expiry via environment variable. Adds TEMPORAL_MAX_SESSION_DURATION (default 2m) and aligns user* cookie MaxAge to min(60s, remaining session time) so cookies never outlive the session boundary.

2. Refresh cookie lifetime incorrectly derived from access token expiry (#3210)
oauth2.Token.Expiry is populated from expires_in in the token response, which per RFC 6749 5.1 and OIDC Core 3.2.2.5 represents the access token lifetime — not the refresh token's. This caused the refresh cookie to expire alongside the access token, making token refresh fail with 401 (confirmed on Keycloak).

The refresh cookie MaxAge is now derived via a priority chain:

  1. JWT exp claim decoded from the refresh token payload — covers Keycloak and other JWT-issuing IdPs automatically
  2. refreshTokenDuration configured per provider (TEMPORAL_AUTH_REFRESH_TOKEN_DURATION) — covers opaque tokens
  3. 7-day default when neither is available

Design Considerations 🎨

  • refreshTokenDuration is placed on AuthProvider (not Auth) since refresh token lifetime is an IdP-specific characteristic
  • JWT exp is decoded without signature verification — only used for cookie lifetime, not for trust decisions
  • Config value acts as an explicit override for operators who know their IdP's refresh token lifetime (e.g. opaque token issuers)

Testing 🧪

How was this tested 👻

  • Unit tests added
  • E2E tests added
  • Manual testing

E2E test suites

Mock OIDC stack (tests/e2e-auth/, docker-compose.e2e-auth.yaml) — short TTLs: access=5s, refresh=30s, session=15s. Covers code paths unique to the mock (opaque token fallback, session timing):

Spec Assertions
refresh-token-duration.spec.ts JWT exp takes priority over refreshTokenDuration config value
session-expiry.spec.ts Refresh succeeds at 6s (access token expired, session alive); user* MaxAge decreases after refresh; refresh fails at 16s (session expired)

Real Keycloak stack (tests/e2e-keycloak/, docker-compose.e2e-keycloak.yaml) — Keycloak 26 with realm import, access=5s, ssoSession=30s, maxSession=25s:

Spec Assertions
login-cookies.spec.ts refresh MaxAge from Keycloak JWT exp (~30s, no config fallback); user* MaxAge ≤ 25s; session_start MaxAge = 25s; HttpOnly flags

Run locally:

# Mock OIDC (9 tests)
docker compose -f docker-compose.e2e-auth.yaml up -d
pnpm test:e2e:auth

# Real Keycloak (5 tests)
docker compose -f docker-compose.e2e-keycloak.yaml up -d
pnpm test:e2e:keycloak

Session expiry:

  1. Set TEMPORAL_MAX_SESSION_DURATION=30s in your Docker environment
  2. Log in to the Temporal UI
  3. Wait 30 seconds — verify you are redirected to re-login with no stale cookie window

Refresh token lifetime (Keycloak or JWT-issuing IdP):

  1. Configure auth with a Keycloak provider, no refreshTokenDuration set
  2. Log in and inspect the refresh cookie MaxAge in browser devtools
  3. Verify it matches the refresh token's exp claim, not the access token lifetime

Refresh token lifetime (opaque token IdP):

  1. Set TEMPORAL_AUTH_REFRESH_TOKEN_DURATION=24h for your provider
  2. Log in and verify the refresh cookie MaxAge is 86400 seconds

Checklists

Draft Checklist

  • maxSessionDuration exposed in docker.yaml
  • refreshTokenDuration added to AuthProvider config
  • jwtExp helper implemented and unit tested
  • Priority chain implemented and all cases tested
  • E2E test suite implemented (9 tests, all passing)
  • Real Keycloak E2E suite added (5 tests via docker-compose.e2e-keycloak.yaml)
  • AUTHENTICATION.md updated

Merge Checklist

  • Tested against a live Keycloak instance (Keycloak 26 E2E suite)

Issue(s) closed

Closes #3223
Closes #3210

Docs

AUTHENTICATION.md updated to document TEMPORAL_MAX_SESSION_DURATION and TEMPORAL_AUTH_REFRESH_TOKEN_DURATION environment variables, the refreshTokenDuration provider config field, and the refresh token lifetime priority chain including a troubleshooting section for opaque vs JWT refresh tokens.

ralf157 added 7 commits March 15, 2026 17:38
  - Expose TEMPORAL_MAX_SESSION_DURATION in docker.yaml (default 2m)
  - SetUser now accepts sessionExpiresAt and caps user* cookie MaxAge
    to min(60s, remaining), preventing cookies from outliving the session
  - authenticateCb passes session expiry at login time
  - refreshTokens derives remaining session time from session_start cookie
    and passes it to SetUser, so the final refresh cycle issues cookies
    that expire exactly when the session does

Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
  The refresh cookie MaxAge was incorrectly set from oauth2.Token.Expiry,
  which per RFC 6749 §5.1 and OIDC Core §3.2.2.5 represents the access
  token lifetime — not the refresh token's. This caused the refresh cookie
  to expire alongside the access token, making token refresh fail with 401.

  Priority chain for refresh cookie MaxAge:
  1. JWT exp claim decoded from the refresh token payload (no sig verify)
  2. refreshTokenDuration configured per provider (covers opaque tokens)
  3. 7-day default when neither is available

  Adds RefreshTokenDuration to AuthProvider config, exposed as
  TEMPORAL_AUTH_REFRESH_TOKEN_DURATION in docker.yaml.

Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
…n test

  Merged TestSetUserRefreshCookieJWTExp into TestSetUserRefreshCookieMaxAge,
  removing the duplicate extractCookieMaxAge helper and renaming makeJWT to
  jwtToken. All opaque and JWT token cases now live in one place with a
  unified struct that carries refreshToken and refreshTokenDuration per case.

Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
…TION.md

  Added refreshTokenDuration to the provider settings reference, a new
  Docker environment variables table covering all TEMPORAL_AUTH_* fields,
  and a troubleshooting note explaining the opaque vs JWT refresh token
  distinction with a config example.

Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
Mock OIDC (9 tests) and real Keycloak (5 tests) stacks covering
user*/refresh/session_start MaxAge, HttpOnly flags, JWT exp priority,
token refresh, and session expiry. All 14 tests passing.

Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
Signed-off-by: ralf157 <ralf.dahmen14@googlemail.com>
@ralf157 ralf157 requested a review from a team as a code owner March 19, 2026 19:43
@ralf157 ralf157 requested review from GraceGardner and removed request for a team March 19, 2026 19:43
@vercel
Copy link

vercel bot commented Mar 19, 2026

@ralf157 is attempting to deploy a commit to the Temporal Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLAassistant commented Mar 19, 2026

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Session expiry not configurable in Docker; user cookies outlive session boundary OIDC Refresh doesn't work due to bad expiration date

2 participants