Skip to content

Fleet sessions live one day, idle out after twelve hours (ADR-0064) - #1508

Merged
jzhao234 merged 1 commit into
mainfrom
session-idle
Sep 15, 2026
Merged

jzhao234 merged 1 commit into
mainfrom
session-idle

Conversation

@jzhao234

Copy link
Copy Markdown
Contributor

TLDR

The elcano_session cookie was a stateless HMAC valid for fourteen days with no notion of inactivity. Under Elcano Auth v2 it is an application session that renews silently through the OIDC handoff while the user's 30-day central session is live, so its job is to bound a stolen cookie and re-check the account with Auth, not to decide how often people log in. It now carries an idle deadline next to the absolute one (one day / twelve hours, the convention Explorer and Lens follow), both are enforced on every request, and the proxy re-mints the cookie on activity. Companion: ElcanoTek/auth#30, ElcanoTek/explorer#23, ElcanoTek/lens#24.

Problem

SessionPayload had a single exp at mint + 14 days. Nothing recorded activity, so idle sessions never ended early, and fourteen days is out of line with the one-day / twelve-hour application-session convention in auth/docs/AUTH_V2_IMPLEMENTATION.md. Fleet is stateless, so an idle limit can only live inside the cookie.

Fix

  • SessionPayload gains idle. exp is the absolute deadline (sessionAbsoluteSeconds, one day), copied on re-mint and never extended; idle is min(now + 12h, exp).
  • verifySessionToken refuses a token whose idle or exp has passed, and a signed token with no idle claim. Pre-deploy cookies are not grandfathered, the same call ADR-0041 made: every user is signed out once at deploy. Central-Auth users are signed back in without a prompt; password users log in once more.
  • refreshSessionCookie re-signs with a later idle when the last mint is over sessionTouchSeconds (60) old, preserving email, exp, epoch, source, issuer, subject, with Max-Age = remaining absolute life and Secure following the request. No-op for elcano_auth sessions, under a minute, or once idle sits on exp.
  • The proxy calls it on every authenticated pass-through (pages and /api), never on redirects, 401s, public routes or bearer-only requests.
  • Both mint paths share signSessionPayload; the mocked e2e cookie minter mirrors the new claim.
  • ADR-0064 records the decision (amends ADR-0041); DEPLOYMENT.md "Ending a session" updated.

Tests

  • auth.test.ts: mint deadlines; idle refusal ahead of exp; refusal without idle; no re-mint under a minute; re-mint after a minute keeps every identity claim and sets httpOnly/lax/secure/path and Max-Age; activity never moves exp; re-minting stops once capped; elcano_auth untouched; Secure follows a plain-HTTP request. Each fails on the previous code.
  • proxy.test.ts: cookie touched exactly once with (request, response, session) on an authenticated pass-through, never otherwise.
  • npm run typecheck, npm run lint, npx vitest run: 1625 passed. The 4 failures in useDashboardData.test.ts (tag-catalogue freshness) fail identically on an untouched origin/main worktree and are unrelated. npm run build and the mocked Playwright suite are left to CI.

🤖 Generated with Claude Code

The elcano_session cookie was a stateless HMAC valid for fourteen days
from login with no notion of inactivity: a laptop left open on Friday
was still signed in the next Friday. Elcano Auth v2 makes Fleet's
session an application session that renews silently through the OIDC
handoff while the user's 30-day central session is live, so its job is
now to bound a stolen cookie and re-check the account with Auth, not to
decide how often people log in. It now carries an idle deadline next to
the absolute one, both enforced on every request, and the proxy
re-mints the cookie on activity.

Problem

SessionPayload had a single exp set to mint + 14 days. Nothing recorded
activity, so idle sessions never ended early, and fourteen days is out
of line with the one-day / twelve-hour convention Explorer and Lens now
follow (auth/docs/AUTH_V2_IMPLEMENTATION.md, "Application session
conventions"). Being stateless, Fleet had no last_seen_at to touch, so
an idle limit could only live inside the cookie.

Fix

- SessionPayload gains `idle`. exp is the absolute deadline
  (sessionAbsoluteSeconds, one day), copied on every re-mint and never
  extended; idle is min(now + sessionIdleSeconds (12h), exp).
- verifySessionToken refuses a token whose idle or exp has passed and
  refuses a signed token with no idle claim. Pre-deploy cookies are not
  grandfathered, the same call ADR-0041 made for claimless cookies: a
  cookie signed for fourteen days flat is exactly what this removes.
  Central-Auth users are signed back in without a prompt; password
  users log in once more.
- refreshSessionCookie re-signs the payload with a later idle when the
  last mint is over sessionTouchSeconds (60) old, preserving email, exp,
  epoch, source, issuer and subject, with Max-Age set to the remaining
  absolute life and Secure following the request. It does nothing for
  elcano_auth sessions (Fleet cannot re-mint Auth's cookie), under a
  minute, or once idle already sits on exp. One minute is the touch
  convention shared with Auth, Explorer and Lens.
- The request proxy calls it on every authenticated pass-through,
  pages and /api alike, and never on redirects, 401s, public routes or
  bearer-only requests.
- Both mint paths share signSessionPayload so neither can emit a cookie
  the verifier refuses; their cookie Max-Age is the absolute lifetime.
- The mocked e2e cookie minter mirrors the new claim.
- ADR-0064 records the decision and amends ADR-0041; DEPLOYMENT.md's
  "Ending a session" note no longer says fourteen days.

Tests

- auth.test.ts: mint deadlines; idle refusal ahead of exp; refusal of a
  signed token without idle; no re-mint under a minute; re-mint after a
  minute keeps every identity claim and sets httpOnly/lax/secure/path
  and Max-Age = remaining absolute; activity never moves exp; re-minting
  stops once idle is capped; elcano_auth sessions untouched; Secure
  follows a plain-HTTP dev request. Each fails on the previous code.
- proxy.test.ts: the cookie is touched exactly once with (request,
  response, session) on an authenticated pass-through and never on
  redirects, 401s, public routes or bearer requests.
- npm run typecheck, npm run lint (oxlint), npx vitest run (full web
  suite). npm run build and the mocked Playwright suite are left to CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jzhao234
jzhao234 merged commit ae0da17 into main Sep 15, 2026
21 checks passed
@jzhao234
jzhao234 deleted the session-idle branch September 15, 2026 14:52
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.

1 participant