fix(auth): align cookie lifetimes with session and refresh token boundaries - #3940
Conversation
The refresh cookie was given the access token's lifetime, and the user* cookies a flat minute regardless of the session boundary. Both are fixed here, along with the missing Docker settings that made the second one impossible to configure. Refresh cookie (#3210). Its MaxAge came from oauth2.Token.Expiry, which is populated from the token response's expires_in. Per RFC 6749 5.1 and OIDC Core 3.2.2.5 that describes the access token, not the refresh token, so the cookie was dropped at the moment the access token expired and the refresh it existed to perform came back 401. The lifetime now comes from the refresh token's own exp claim where the provider issues a JWT, then from a new per-provider refreshTokenDuration for providers that issue opaque tokens, then a 7 day default. The existing 30 day cap is kept. User cookies (#3223). They were always issued for 60 seconds, so a refresh shortly before the session boundary left the browser holding credentials the server had already stopped honouring: a signed-in UI whose every API call returned 401. They are now clamped to whatever is left of the session. Docker config (#3223). maxSessionDuration was enforced but absent from docker.yaml, so it could not be set without a wholly custom config file. Both it and refreshTokenDuration are now exposed, and both default to unset so existing deployments are unaffected. Also corrects docs advising `maxSessionDuration: 0`, which yaml.v3 rejects as an int rather than a duration. The working spelling is `0s`.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Verified end to end against a running stackReal Chrome → real Demo config, chosen to make the failure quick rather than to be realistic: mock IdP access token 10s, refresh token 24h,
Both numbers are read from the literal That 401 is exactly #3210 as reported: the refresh cookie is gone at the moment it is needed, so the browser sends nothing and the user is signed out mid-session. The 60s vs 45s row is the #3223 cookie half — a signed-in UI whose every API call returns 401. Recording of both runs to follow. |
The Go tests added alongside the fix drive the refresh handler in process. That covers the logic but stops short of the thing users actually hit: a browser, a real identity provider, and cookies the Go server writes as Set-Cookie headers. tests/integration/oauth-flow.spec.ts cannot fill the gap either, since it mocks /auth/sso and /auth/refresh outright, which is exactly what would need to be real. This starts a second, auth-enabled ui-server on 8081 alongside the existing E2E one, backed by the repo's own mock OIDC provider, and drives a real login against it. The e2e CI job already builds the Go server and runs the Playwright suite, so this needs no new job or infrastructure. Three of the four tests fail against the previous behaviour: the refresh cookie carries the access token's 5s lifetime rather than 24h, the user0 cookie is issued for 60s against a 45s session, and the refresh after the access token expires returns 401. The fourth asserts that session expiry still ends a session, and passes either way, so that lengthening the cookies cannot quietly disable maxSessionDuration. The ui-server test harness tracked one server in a module level variable, which two concurrent servers would clobber, leaving the first without a handle to shut down. It is now keyed by env.
Added a real end-to-end testCorrecting my earlier wording: the Go tests I first described as end-to-end are integration tests of the refresh handler. Useful, but they never touch a browser or a real identity provider. There is now a genuine E2E test as well.
I looked at extending Against the previous behaviour, three of the four fail: The one that passes both ways is deliberate — it waits past Full E2E suite is 37 passed with this in, so the second server and the harness change break nothing. One harness change worth a reviewer's eye: |
Map.get returns UIServer | undefined. The previous module level variable was typed as UIServer and hid that, so keying the registry surfaced a strict mode error the old shape had been papering over. The return type now says what it returns, and the one caller that assumed a server handles its absence.
brandenco
left a comment
There was a problem hiding this comment.
not sure if my approval matters but thank you! this has been an annoying bug on our temporal instance for a while
Auto-generated version bump from 2.54.1 to 2.55.0 Bump type: minor Changes included: - [`79ca6514`](79ca651) Add size variants to IO badges (#3908) - [`6cc68ed0`](6cc68ed) feat(deployments): show worker deployment connection status by default (#3852) - [`c0885b3d`](c0885b3) Agents Product page (#3904) - [`8b9054d6`](8b9054d) Consistent headers (#3909) - [`4d8aa361`](4d8aa36) Fix test (#3911) - [`6d6cb9fc`](6d6cb9f) Remove drop shadow from loading spinner (#3910) - [`542fa634`](542fa63) Fix portaled menu anchor scaling (#3913) - [`c71816bf`](c71816b) feat(workers): add Amazon Bedrock AgentCore as a compute provider (#3884) - [`d179959c`](d179959) Make nexus registry description reusable (#3914) - [`12526241`](1252624) Make workflow details task queue a filter link (#3922) - [`3c479755`](3c47975) Polish Agents page cards, header and Integrations text (#3917) - [`e8a0d81d`](e8a0d81) chore(deps): resolve open Dependabot security alerts (#3920) - [`7a47d7ee`](7a47d7e) Remove filter button corner rounding (#3926) - [`a561ce12`](a561ce1) docs: add serverless-worker-aws skill (#3888) - [`550056b1`](550056b) Render Markdown links in Event summary (#3865) - [`5699aacd`](5699aac) Generate the render stylesheet so both routes share one source (#3905) - [`4eb3b075`](4eb3b07) Fix clipped timeline dot icons on 1x displays (#3929) - [`bff55086`](bff5508) (fix) Keep the Cloud nav toggle square when the nav is collapsed (#3930) - [`ea9ee2b4`](ea9ee2b) Add Scheduled system view and last viewed Saved Views (#3924) - [`8094a997`](8094a99) Fix last viewed saved view tracking (#3934) - [`dc4c3058`](dc4c305) Match the Cloud nav header to the Web UI's logo and title (#3933) - [`e97e6648`](e97e664) (fix) Web UI nav: toggle size, divider spacing, and version contrast (#3932) - [`4486bd76`](4486bd7) Set the markdown frame in the system font stack (#3935) - [`0e0ccedb`](0e0cced) fix(pagination): honor the url page when the store is rebuilt (FE-538) (#3875) - [`be938c70`](be938c7) Fix workflow summary iframe navigation history (#3937) - [`9c9685a3`](9c9685a) Update serverless-worker-create form for AgentCore (#3936) - [`00d02395`](00d0239) FE-734 - Add resizable columns to the configurable tables (#3925) - [`93d0fb2c`](93d0fb2) Add configurable UI extension slots (#3622) - [`80d864d1`](80d864d) Collapse common errors by default (#3921) - [`ffee0375`](ffee037) Fix multiline timeline summaries (#3944) - [`52f5a3e4`](52f5a3e) Include 1000 for pageSize (#3938) - [`5b4fff31`](5b4fff3) Color the favicon by the status of the workflow being viewed (#3900) - [`be382aa3`](be382aa) fix(auth): align cookie lifetimes with session and refresh token boundaries (#3940) - [`14f25d35`](14f25d3) fix(theme): ship the 2xs font size token to consumers (#3947) - [`737f55af`](737f55a) fix(workers): resolve worker deployment from task queue (#3883) - [`86f48ed0`](86f48ed) fix(workers): describe pollers, not slots, in the poller tooltip (#3956) - [`c1829dcf`](c1829dc) fix(copyable): round the copy button's corners (#3954) - [`9c20e7cf`](9c20e7c) Update links for agents (#3957) - [`c41a20f0`](c41a20f) Fix black area below the login page in dark mode (#3953) - [`82587bd8`](82587bd) Holographic upgrade sticker in the side nav (#3949) - [`6d27e4ce`](6d27e4c) Move Cloud Run to public preview (#3945) - [`31f5ab2f`](31f5ab2) Enable antialiasing on the body text (#3964) - [`78ef7abf`](78ef7ab) Use normal weight and primary text for worker details stats - [`a66748b8`](a66748b) Remove redundant text-primary - [`b03ba040`](b03ba04) fix(holocene): align label-to-field gap across form controls (#3968) - [`32a0273b`](32a0273) a11y(1.4.3): use text-secondary for Input and Combobox hint text (#3970) - [`838c0d25`](838c0d2) feat(activities): show updated activity options in the Pending Activity card (#3966) - [`2edc463c`](2edc463) Register render route without public path prefix (#3973)
Auto-generated version bump from 2.54.1 to 2.55.0 Bump type: minor Changes included: - [`79ca6514`](79ca651) Add size variants to IO badges (#3908) - [`6cc68ed0`](6cc68ed) feat(deployments): show worker deployment connection status by default (#3852) - [`c0885b3d`](c0885b3) Agents Product page (#3904) - [`8b9054d6`](8b9054d) Consistent headers (#3909) - [`4d8aa361`](4d8aa36) Fix test (#3911) - [`6d6cb9fc`](6d6cb9f) Remove drop shadow from loading spinner (#3910) - [`542fa634`](542fa63) Fix portaled menu anchor scaling (#3913) - [`c71816bf`](c71816b) feat(workers): add Amazon Bedrock AgentCore as a compute provider (#3884) - [`d179959c`](d179959) Make nexus registry description reusable (#3914) - [`12526241`](1252624) Make workflow details task queue a filter link (#3922) - [`3c479755`](3c47975) Polish Agents page cards, header and Integrations text (#3917) - [`e8a0d81d`](e8a0d81) chore(deps): resolve open Dependabot security alerts (#3920) - [`7a47d7ee`](7a47d7e) Remove filter button corner rounding (#3926) - [`a561ce12`](a561ce1) docs: add serverless-worker-aws skill (#3888) - [`550056b1`](550056b) Render Markdown links in Event summary (#3865) - [`5699aacd`](5699aac) Generate the render stylesheet so both routes share one source (#3905) - [`4eb3b075`](4eb3b07) Fix clipped timeline dot icons on 1x displays (#3929) - [`bff55086`](bff5508) (fix) Keep the Cloud nav toggle square when the nav is collapsed (#3930) - [`ea9ee2b4`](ea9ee2b) Add Scheduled system view and last viewed Saved Views (#3924) - [`8094a997`](8094a99) Fix last viewed saved view tracking (#3934) - [`dc4c3058`](dc4c305) Match the Cloud nav header to the Web UI's logo and title (#3933) - [`e97e6648`](e97e664) (fix) Web UI nav: toggle size, divider spacing, and version contrast (#3932) - [`4486bd76`](4486bd7) Set the markdown frame in the system font stack (#3935) - [`0e0ccedb`](0e0cced) fix(pagination): honor the url page when the store is rebuilt (FE-538) (#3875) - [`be938c70`](be938c7) Fix workflow summary iframe navigation history (#3937) - [`9c9685a3`](9c9685a) Update serverless-worker-create form for AgentCore (#3936) - [`00d02395`](00d0239) FE-734 - Add resizable columns to the configurable tables (#3925) - [`93d0fb2c`](93d0fb2) Add configurable UI extension slots (#3622) - [`80d864d1`](80d864d) Collapse common errors by default (#3921) - [`ffee0375`](ffee037) Fix multiline timeline summaries (#3944) - [`52f5a3e4`](52f5a3e) Include 1000 for pageSize (#3938) - [`5b4fff31`](5b4fff3) Color the favicon by the status of the workflow being viewed (#3900) - [`be382aa3`](be382aa) fix(auth): align cookie lifetimes with session and refresh token boundaries (#3940) - [`14f25d35`](14f25d3) fix(theme): ship the 2xs font size token to consumers (#3947) - [`737f55af`](737f55a) fix(workers): resolve worker deployment from task queue (#3883) - [`86f48ed0`](86f48ed) fix(workers): describe pollers, not slots, in the poller tooltip (#3956) - [`c1829dcf`](c1829dc) fix(copyable): round the copy button's corners (#3954) - [`9c20e7cf`](9c20e7c) Update links for agents (#3957) - [`c41a20f0`](c41a20f) Fix black area below the login page in dark mode (#3953) - [`82587bd8`](82587bd) Holographic upgrade sticker in the side nav (#3949) - [`6d27e4ce`](6d27e4c) Move Cloud Run to public preview (#3945) - [`31f5ab2f`](31f5ab2) Enable antialiasing on the body text (#3964) - [`78ef7abf`](78ef7ab) Use normal weight and primary text for worker details stats - [`a66748b8`](a66748b) Remove redundant text-primary - [`b03ba040`](b03ba04) fix(holocene): align label-to-field gap across form controls (#3968) - [`32a0273b`](32a0273) a11y(1.4.3): use text-secondary for Input and Combobox hint text (#3970) - [`838c0d25`](838c0d2) feat(activities): show updated activity options in the Pending Activity card (#3966) - [`2edc463c`](2edc463) Register render route without public path prefix (#3973) - [`7d564fe5`](7d564fe) Render event summaries on one line in the timeline and event history (#3972)
Auto-generated version bump from 2.54.1 to 2.55.0 Bump type: minor Changes included: - [`79ca6514`](79ca651) Add size variants to IO badges (#3908) - [`6cc68ed0`](6cc68ed) feat(deployments): show worker deployment connection status by default (#3852) - [`c0885b3d`](c0885b3) Agents Product page (#3904) - [`8b9054d6`](8b9054d) Consistent headers (#3909) - [`4d8aa361`](4d8aa36) Fix test (#3911) - [`6d6cb9fc`](6d6cb9f) Remove drop shadow from loading spinner (#3910) - [`542fa634`](542fa63) Fix portaled menu anchor scaling (#3913) - [`c71816bf`](c71816b) feat(workers): add Amazon Bedrock AgentCore as a compute provider (#3884) - [`d179959c`](d179959) Make nexus registry description reusable (#3914) - [`12526241`](1252624) Make workflow details task queue a filter link (#3922) - [`3c479755`](3c47975) Polish Agents page cards, header and Integrations text (#3917) - [`e8a0d81d`](e8a0d81) chore(deps): resolve open Dependabot security alerts (#3920) - [`7a47d7ee`](7a47d7e) Remove filter button corner rounding (#3926) - [`a561ce12`](a561ce1) docs: add serverless-worker-aws skill (#3888) - [`550056b1`](550056b) Render Markdown links in Event summary (#3865) - [`5699aacd`](5699aac) Generate the render stylesheet so both routes share one source (#3905) - [`4eb3b075`](4eb3b07) Fix clipped timeline dot icons on 1x displays (#3929) - [`bff55086`](bff5508) (fix) Keep the Cloud nav toggle square when the nav is collapsed (#3930) - [`ea9ee2b4`](ea9ee2b) Add Scheduled system view and last viewed Saved Views (#3924) - [`8094a997`](8094a99) Fix last viewed saved view tracking (#3934) - [`dc4c3058`](dc4c305) Match the Cloud nav header to the Web UI's logo and title (#3933) - [`e97e6648`](e97e664) (fix) Web UI nav: toggle size, divider spacing, and version contrast (#3932) - [`4486bd76`](4486bd7) Set the markdown frame in the system font stack (#3935) - [`0e0ccedb`](0e0cced) fix(pagination): honor the url page when the store is rebuilt (FE-538) (#3875) - [`be938c70`](be938c7) Fix workflow summary iframe navigation history (#3937) - [`9c9685a3`](9c9685a) Update serverless-worker-create form for AgentCore (#3936) - [`00d02395`](00d0239) FE-734 - Add resizable columns to the configurable tables (#3925) - [`93d0fb2c`](93d0fb2) Add configurable UI extension slots (#3622) - [`80d864d1`](80d864d) Collapse common errors by default (#3921) - [`ffee0375`](ffee037) Fix multiline timeline summaries (#3944) - [`52f5a3e4`](52f5a3e) Include 1000 for pageSize (#3938) - [`5b4fff31`](5b4fff3) Color the favicon by the status of the workflow being viewed (#3900) - [`be382aa3`](be382aa) fix(auth): align cookie lifetimes with session and refresh token boundaries (#3940) - [`14f25d35`](14f25d3) fix(theme): ship the 2xs font size token to consumers (#3947) - [`737f55af`](737f55a) fix(workers): resolve worker deployment from task queue (#3883) - [`86f48ed0`](86f48ed) fix(workers): describe pollers, not slots, in the poller tooltip (#3956) - [`c1829dcf`](c1829dc) fix(copyable): round the copy button's corners (#3954) - [`9c20e7cf`](9c20e7c) Update links for agents (#3957) - [`c41a20f0`](c41a20f) Fix black area below the login page in dark mode (#3953) - [`82587bd8`](82587bd) Holographic upgrade sticker in the side nav (#3949) - [`6d27e4ce`](6d27e4c) Move Cloud Run to public preview (#3945) - [`31f5ab2f`](31f5ab2) Enable antialiasing on the body text (#3964) - [`78ef7abf`](78ef7ab) Use normal weight and primary text for worker details stats - [`a66748b8`](a66748b) Remove redundant text-primary - [`b03ba040`](b03ba04) fix(holocene): align label-to-field gap across form controls (#3968) - [`32a0273b`](32a0273) a11y(1.4.3): use text-secondary for Input and Combobox hint text (#3970) - [`838c0d25`](838c0d2) feat(activities): show updated activity options in the Pending Activity card (#3966) - [`2edc463c`](2edc463) Register render route without public path prefix (#3973) - [`7d564fe5`](7d564fe) Render event summaries on one line in the timeline and event history (#3972) Co-authored-by: laurakwhit <15069288+laurakwhit@users.noreply.github.com>
Description & motivation 💭
Supersedes #3235, rebuilt from current
main. That PR was opened from a fork'smainbranch, so it could not be updated in place, andSetUserhas since gained asecureparameter. Thanks to @ralf157 for the original diagnosis and fix — the analysis in #3235 is what this is built on.auth-cookie-lifetimes.mp4
Two bugs, both confirmed still present on
main:1. Refresh cookie expires with the access token (#3210)
The
refreshcookie'sMaxAgecame fromoauth2.Token.Expiry, which is populated from the token response'sexpires_in. Per RFC 6749 §5.1 and OIDC Core §3.2.2.5 that describes the access token. The cookie was therefore dropped at the exact moment the access token expired, so the refresh it exists to perform arrived with no cookie and came back 401. Reported against Keycloak, reproducible on any IdP whose refresh token outlives its access token.The lifetime now resolves in this order:
expclaim, when the IdP issues a JWT refresh token (Keycloak and friends — no configuration needed).refreshTokenDuration, for IdPs that issue opaque refresh tokens whose lifetime the server cannot read.The existing 30-day cap is kept.
2. User cookies outlive the session boundary (#3223)
user*cookies were always issued for a flat 60 seconds. A refresh performed shortly beforemaxSessionDurationelapsed handed the browser a full minute of credentials the server had already stopped honouring — a UI that looks signed in while every API call behind it returns 401. They are now clamped tomin(60s, time left in session).3.
maxSessionDurationunreachable in Docker (#3223)The field was enforced server-side but absent from
docker.yaml, so it could not be set without supplying a wholly custom config file. Both it andrefreshTokenDurationare now exposed as environment variables.Design Considerations 🎨
#3235defaultedTEMPORAL_MAX_SESSION_DURATIONto2m, copied from the local dev config, which would have silently started logging every Docker deployment out every two minutes.TestDockerConfigSessionDefaultsAreUnsetpins this.refreshTokenDurationsits onAuthProviderrather thanAuth, since refresh token lifetime is a property of the IdP.expis read without signature verification, because it only chooses a cookie lifetime. Nothing is trusted on the strength of it — the IdP still validates the token on every refresh, and a forgedexpcan only make the browser drop a cookie earlier or later than it needed to. The comment onjwtExpsays so.SetUser's three positional booleans/durations would have grown to five, so the trailing arguments are now aCookieOptionsstruct. Only two call sites.with-auth.yamlboth advisemaxSessionDuration: 0for "unlimited", whichyaml.v3rejects (cannot unmarshal !!int 0 into time.Duration). Corrected to0s.Scope vs #3235
#3235 also added ~1100 lines of new E2E infrastructure: two
docker-composestacks, a Keycloak 26 realm import, two extra Playwright configs and a duplicate mock-OIDC config, none of it wired into CI. That is not carried over.The equivalent coverage here reuses what the repo already has. The
e2e-testsjob already builds the Go server and runs Playwright against a realui-server, andutilities/oidc-serveris already a working identity provider, so the browser-level coverage cost one config file, one harness module and one spec, inside CI jobs that already run.Testing 🧪
How was this tested 👻
End to end (
tests/e2e/auth-cookie-lifetimes.spec.ts)Real Chromium, a real login at the repo's own mock OIDC provider, and a real auth-enabled
ui-serverprocess. The cookie lifetimes under test are decided by the Go server and reach the browser only asSet-Cookieheaders, so the assertions read those headers directly.tests/integration/oauth-flow.spec.tscould not be extended for this: it mocks/auth/ssoand/auth/refreshwithpage.route, which is precisely what has to be real here.The
e2e-testsCI job already builds the Go server and runs Playwright, so this adds no new CI job and no new infrastructure.tests/global-setup.tsstarts a second, auth-enabledui-serveron 8081 alongside the existing one on 8080, plus the mock provider, viautilities/auth-e2e-stack.tsand a newserver/config/e2e-auth.yaml.refresh cookie outlives the access tokenrefreshMax-Age is 24h, not the 5s access token lifetimeuser cookies do not outlive the sessionuser0Max-Age ≤ the 45smaxSessionDuration, and under the 60s defaulttoken refresh succeeds after the access token has expiredGET /auth/refresh→ 200refresh is refused once the session has expiredmaxSessionDuration, then/auth/refresh→ 401Against the previous behaviour, the first three fail and the fourth passes:
That fourth test is the guard: lengthening the cookies must not quietly turn
maxSessionDurationinto a limit that no longer ends a session, so it has to pass both before and after.Full E2E suite: 37 passed, so the second server and the harness change break nothing.
Integration (
server/server/route/auth_cookies_test.go)Six Go tests that call the
refreshTokenshandler in process against anhttptestidentity provider — real HTTP token exchange, realoauth2client, real cookie serialization. Faster than the browser tests and covers the opaque-token and default-fallback branches that the mock provider alone cannot reach.TestRefreshCookieOutlivesTheAccessTokenexpires_in=5while issuing a 7-day JWT refresh token → the cookie tracks the token'sexpTestRefreshCookieForOpaqueTokenUsesConfiguredDurationrefreshTokenDurationis usedTestRefreshCookieForOpaqueTokenFallsBackToDefaultTestUserCookiesNeverOutliveTheSessionuser0Max-Age is 30, not 60TestUserCookiesKeepDefaultWhenSessionHasRoomuser0Max-Age stays 60TestUserCookiesUnaffectedWithoutMaxSessionDurationuser0Max-Age stays 60The four bug-specific ones fail against the old logic; the two "nothing should change" ones pass against both.
Unit
server/server/auth/cookie_test.gocoversjwtExp(valid, fractionalexp, base64 padding, and ten rejection cases including opaque tokens and malformed JWTs), the fullrefreshCookieMaxAgepriority chain including both cap paths and an already-expiredexp, and theuserCookieMaxAgeboundaries.server/plugins/fs_config_provider/loader_test.goloads the realdocker.yamlthrough the real template pipeline, pinning the unset defaults and checking both environment variables take effect.Not tested here
No live Keycloak. The Keycloak-specific behaviour is the JWT
expbranch, exercised byTestRefreshCookieOutlivesTheAccessTokenwith a Keycloak-shaped refresh token, but that is a stand-in rather than the real server — the mock provider issues opaque refresh tokens, so the browser tests cover the configured-duration branch instead. @ralf157 tested #3235 against live Keycloak 26; a confirmation on this branch would be welcome.Checklists
Merge Checklist
Issue(s) closed
Closes #3210
Closes #3223
Docs
AUTHENTICATION.mdgains a Refresh token lifetime section describing the priority chain and whyexpires_inis not used, a Docker Environment Variables table for the auth settings, a note thatuser*cookies are held to the session boundary, and a troubleshooting entry for refresh failing with 401 at access-token expiry.