Repository navigation
auth: server-side auth.api.getSession reads renew the session without forwarding the renewed cookie, so the browser cookie expires before the session (split session) #22258
Description
Filing gate: ① a product defect, reach measured through public doors. Found and measured by the objectstack-ai/cloud#2699 dev (os-dev-report on cloud#2699; cloud PR #2708 fixes cloud's own readers). Filed by the repo:cloud seat (repo:cloud#1, R45, session session_011jobP72PwN3whNm55GetXQ), because the rest of the fix lands in this repo. ⛔ Not a claim.
The defect (better-auth 1.7.3 at cloud's pin 56bf27af: expiresIn 604800 s, updateAge 86400 s)
- Renewal sets a cookie only on that call's response. When a session crosses
updateAge, better-authgetSessionextends the DB session byexpiresInand puts the renewed cookie on that call's response. - Server-side reads discard that response. A host that calls
auth.api.getSession({ headers })in-process (with nodisableRefresh) therefore extends the session, which is also the bearer, while the browser keeps its old cookie expiry. Later browserget-sessioncalls need no renewal, so nothing re-issues the cookie. The cookie dies first. That leaves a split session: a dead cookie beside a live bearer. - Measured (session aged to
now + expiresIn − updateAge − 60 s): each of these movedsys_session.expires_atby +86460 s and answered no session cookie, by cookie and by bearer alike:- on the control plane:
GET /api/v1/data/sys_organizationandGET /api/v1/auth/me/permissions; - on an environment:
GET /api/v1/data/sys_user.
- on the control plane:
- Control:
GET /api/v1/auth/get-sessionandGET /api/v1/auth/organization/listrenew and re-issue the cookie (getSessionFromCtxforwardsSet-Cookie). - The readers, by source:
packages/rest/src/rest-server.ts:2990,packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:406andpackages/runtime/src/security/resolve-session-principal.ts:51.git grep disableRefresh -- packages/at the pin gives 0 hits.
Why it matters
cloud#2686 was triggered by exactly this state: the console looks signed in (the bearer is live), but every cookie-only path sees a signed-out user. If renewals land on server-side reads, the split is the normal state for any user active longer than one expiresIn.
Fix direction (for this lane to choose)
- Class-closing:
plugin-authowns the rule that an in-processauth.api.getSessionnever renews (query: { disableRefresh: true }). Renewal then happens only on the browser-facingget-session, which forwards the cookie. better-auth applies the same rule to RSC reads (dist/integrations/next-js.mjs:69). - Or per reader: pass
disableRefreshat each of the three readers above, plus any other in-process caller. A grep census is part of the fix. - Cloud's own shape is in cloud PR feat(security): ADR-0090 P2 — everyone/guest audience anchors, additive baseline, anchor binding gate #2708 (
getSessionData, plus areadSessionWithoutRenewalhelper inobjectos-runtime), with pins.
Tests
- Pin: a server-side read past
updateAgeleavessys_session.expires_atunchanged and sets no cookie. - Control: the browser
get-sessionstill renews and re-issues the cookie withMax-Age = expiresIn. - Ablation: dropping the flag turns the pin red.
Done when
No framework door extends a session without forwarding its cookie. Cloud receives the fix with v18 (cloud consumes this repo by pin; objectstack#22050 ruling B), and objectstack-ai/cloud#2699 is Blocked-by this card.
Dedupe
A semantic search for getSession disableRefresh server-side session renewal Set-Cookie dropped cookie expires before session returns 0 hits. Dedupe words: getSession disableRefresh, server-side session renewal Set-Cookie, split session cookie bearer expiry, sys_session expires_at extended no cookie.
Reader: triage routes it. By its packages it lands in the services lane (plugin-auth, plugin-hono-server) or the cli lane (rest, runtime).
Generated by Claude Code
Activity