Skip to content

Explorer sessions default to 24 hours absolute, 12 hours idle - #23

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

jzhao234 merged 1 commit into
mainfrom
session-lifetimes

Conversation

@jzhao234

Copy link
Copy Markdown
Contributor

TLDR

While the central Auth session is live, an expired Explorer session is renewed by a silent redirect through the code handoff, so Explorer's own limits govern stolen-cookie blast radius, not login frequency. With Auth moving to a 30-day central session (ElcanoTek/auth#30), Explorer's defaults move from 12h absolute / 60min idle to 24h absolute / 12h idle.

Problem

The 60-minute idle limit was chosen when this session was the login users felt. It is not any more; the app limit only bounds a stolen cookie and sets how often Explorer re-checks the account with Auth. Sixty minutes bought nothing over twelve hours for that and forced a redirect plus access-list check after every break.

Fix

  • DEFAULT_IDLE_SECONDS 3600 → 43200, DEFAULT_ABSOLUTE_SECONDS 43200 → 86400. EXPLORER_SESSION_* env knobs unchanged; bootstrap.sh and .env.example carry the new defaults.
  • Comment at the constants explains the two layers and points to Auth's "Application session conventions".
  • README and DEPLOYMENT tables and prose updated.

Tests

  • Set-Cookie test asserts Max-Age=86400; store-level idle/touch tests use explicit values and are unchanged.
  • ruff check, ruff format --check, pytest (113 passed).

🤖 Generated with Claude Code

Explorer's app session no longer needs to carry the "how often do users
log in" burden: while the central Auth session is live, an expired
Explorer session is renewed by a silent redirect through the code
handoff. Auth's central session is moving to 30 days, so Explorer's
own session becomes the short, bounding one at one day absolute and
12 hours idle, replacing 12 hours and 60 minutes.

Problem

The 60-minute idle limit was set when the Explorer session was the
login users felt. With Auth at 30 days it is not; the app limit only
governs how long a stolen Explorer cookie is useful and how often
Explorer re-checks with Auth that the account is still enabled. Sixty
minutes of idle bought nothing over 12 hours for that purpose, while
forcing a redirect and fresh access-list check after every lunch break.

Fix

- DEFAULT_IDLE_SECONDS 3600 -> 43200, DEFAULT_ABSOLUTE_SECONDS
  43200 -> 86400. The EXPLORER_SESSION_* env knobs are unchanged and
  still override; bootstrap.sh and .env.example carry the new defaults.
- A comment at the constants explains the two-layer model and points to
  Auth's "Application session conventions", which records the rule for
  every service.
- README and DEPLOYMENT tables and the session prose updated to match.

Tests

- The Set-Cookie test asserts Max-Age=86400; the store-level idle and
  touch tests build the store with explicit values and are unchanged.
- ruff check, ruff format --check, pytest (113 passed).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jzhao234
jzhao234 merged commit 5b44d66 into main Sep 15, 2026
4 checks passed
@jzhao234
jzhao234 deleted the session-lifetimes branch September 15, 2026 14:52
jzhao234 added a commit to ElcanoTek/fleet that referenced this pull request Sep 15, 2026
…1508)

## 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](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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