From f2b175d942ac732983d8928febcfb80200e350ea Mon Sep 17 00:00:00 2001 From: jzhao234 Date: Tue, 15 Sep 2026 14:29:57 +0000 Subject: [PATCH] Explorer sessions default to 24 hours absolute, 12 hours idle 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 --- .env.example | 7 ++++--- README.md | 9 ++++++--- app/central_auth.py | 13 ++++++++++--- docs/DEPLOYMENT.md | 4 ++-- scripts/bootstrap.sh | 4 ++-- tests/test_central_auth_endpoints.py | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 954e52c..0e1a1ca 100644 --- a/.env.example +++ b/.env.example @@ -70,9 +70,10 @@ EXPLORER_ACCESS_DB="/var/lib/explorer/access.db" # The __Host- cookie contract requires HTTPS; central mode fails startup if # this is disabled. AUTH_ALLOW_INSECURE_HTTP exists only for development. EXPLORER_AUTH_COOKIE_SECURE="1" -# Idle and absolute session lifetimes: 60 minutes and 12 hours. -EXPLORER_SESSION_IDLE_SECONDS="3600" -EXPLORER_SESSION_ABSOLUTE_SECONDS="43200" +# Idle and absolute session lifetimes: 12 hours and 24 hours. Short on +# purpose: expiry is a silent redirect while the central Auth session lives. +EXPLORER_SESSION_IDLE_SECONDS="43200" +EXPLORER_SESSION_ABSOLUTE_SECONDS="86400" # ── Session and CSRF cookie ───────────────────────────────────────────── # Signs the central-login state/PKCE transaction plus the short-lived cookie diff --git a/README.md b/README.md index f936d9f..04ff0a1 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,8 @@ Neither dotenv file is committed. `.env.example` is the annotated template: | `EXPLORER_ACCESS_DB` | central mode | `/var/lib/explorer/access.db` | Deployment-local email access list and app sessions; contains no passwords. | | `EXPLORER_AUTH_COOKIE_SECURE` | central mode | `1` | Requires the app-scoped cookie to travel over HTTPS. Keep enabled in production. | | `EXPLORER_UI_COOKIE_SECURE` | no | `1` | Requires the search/CSRF cookie to travel over HTTPS. Keep enabled in production. | -| `EXPLORER_SESSION_IDLE_SECONDS` | no | `3600` | Central-mode Explorer session idle lifetime (60 minutes). | -| `EXPLORER_SESSION_ABSOLUTE_SECONDS` | no | `43200` | Central-mode Explorer session absolute lifetime (12 hours). | +| `EXPLORER_SESSION_IDLE_SECONDS` | no | `43200` | Central-mode Explorer session idle lifetime (12 hours). | +| `EXPLORER_SESSION_ABSOLUTE_SECONDS` | no | `86400` | Central-mode Explorer session absolute lifetime (24 hours). | | `EXPLORER_SESSION_SECRET` | central: **yes**; Elcano: recommended | a dev placeholder | Signs central login state and the cookie scoping search jobs to one browser. Generate with `openssl rand -hex 32`. | ### Authentication @@ -200,9 +200,12 @@ sudo explorer access revoke user@example.com Allowed users receive a random 256-bit, app-only session. Only its SHA-256 hash is stored in `/var/lib/explorer/access.db`; the host-only `__Host-explorer_session` cookie is `Secure`, `HttpOnly`, `SameSite=Lax`, and -scoped to `/`. Sessions expire after 60 minutes idle or 12 hours total; the +scoped to `/`. Sessions expire after 12 hours idle or 24 hours total; the idle clock is refreshed at most once a minute (the Elcano convention for service sessions), so a session can end up to a minute early but never late. +Expiry costs the user only a redirect: while their central Auth session (30 +days) is live, the handoff signs them back in without a prompt. The short +app limit bounds a stolen cookie and re-checks the account with Auth daily. Revoking an email immediately invalidates all of that email's Explorer sessions. Logout is CSRF-protected and revokes only the current Explorer session. Auth's signed back-channel endpoint also revokes every local session diff --git a/app/central_auth.py b/app/central_auth.py index 26e88d7..e1e71c5 100644 --- a/app/central_auth.py +++ b/app/central_auth.py @@ -31,11 +31,18 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey CENTRAL_AUTH_COOKIE_NAME = "__Host-explorer_session" -DEFAULT_IDLE_SECONDS = 60 * 60 -DEFAULT_ABSOLUTE_SECONDS = 12 * 60 * 60 +# Application sessions are deliberately short. Expiry costs the user only a +# redirect: the code handoff signs them back in silently while the 30-day +# central Auth session is live. The short limit bounds a stolen Explorer +# cookie and forces a daily re-check with Auth that the account is still +# enabled. One day absolute, 12 hours idle is the Elcano convention for every +# application session (owner decision 2026-09-15; see Auth's +# docs/AUTH_V2_IMPLEMENTATION.md "Application session conventions"). +DEFAULT_IDLE_SECONDS = 12 * 60 * 60 +DEFAULT_ABSOLUTE_SECONDS = 24 * 60 * 60 # How often a validated session rewrites last_seen_at / idle_expires_at. Every # request reads the session; only a request more than this long after the -# previous touch writes. The idle limit therefore behaves as "60 minutes minus +# previous touch writes. The idle limit therefore behaves as "12 hours minus # at most one minute", never longer, and a page's burst of requests costs one # SQLite write instead of one per request. One minute is the convention for # every Elcano service with its own sessions (Auth, Explorer, Lens, and diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f1c28c8..242b419 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -180,8 +180,8 @@ but lose to `.env`. | `AUTH_SIGNING_PREVIOUS_PUBKEYS` | no | *(empty)* | Comma-separated prior Ed25519 public keys. Rarely needed now: in central mode Explorer also reads Auth's published `/jwks.json` (cached 10 minutes, refreshed once when a token names an unknown key), so an Auth key rotation needs no env edit here. | | `EXPLORER_ACCESS_DB` | central mode | `/var/lib/explorer/access.db` | SQLite email access list and Explorer session hashes. Keep it outside the application tree and mode `0600`. | | `EXPLORER_AUTH_COOKIE_SECURE` | central mode | `1` | Controls `Secure` on `__Host-explorer_session`. Central mode refuses an insecure setting in production. | -| `EXPLORER_SESSION_IDLE_SECONDS` | no | `3600` | Explorer app-session idle lifetime. Activity refreshes this deadline but never extends the absolute deadline. | -| `EXPLORER_SESSION_ABSOLUTE_SECONDS` | no | `43200` | Explorer app-session absolute lifetime. | +| `EXPLORER_SESSION_IDLE_SECONDS` | no | `43200` | Explorer app-session idle lifetime. Activity refreshes this deadline but never extends the absolute deadline. | +| `EXPLORER_SESSION_ABSOLUTE_SECONDS` | no | `86400` | Explorer app-session absolute lifetime. | Central mode owns no passwords. Manage only the local authorization list: diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index ce52cbe..4c79fc8 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -164,8 +164,8 @@ AUTH_SIGNING_PUBKEY="${AUTH_SIGNING_PUBKEY:-}" AUTH_SIGNING_PUBKEY="$(prompt AUTH_SIGNING_PUBKEY "auth service AUTH_SIGNING_PUBKEY, base64 (run 'auth pubkey' on the auth host; blank to set later)" "$AUTH_SIGNING_PUBKEY")" EXPLORER_ACCESS_DB="${EXPLORER_ACCESS_DB:-/var/lib/explorer/access.db}" -EXPLORER_SESSION_IDLE_SECONDS="${EXPLORER_SESSION_IDLE_SECONDS:-3600}" -EXPLORER_SESSION_ABSOLUTE_SECONDS="${EXPLORER_SESSION_ABSOLUTE_SECONDS:-43200}" +EXPLORER_SESSION_IDLE_SECONDS="${EXPLORER_SESSION_IDLE_SECONDS:-43200}" +EXPLORER_SESSION_ABSOLUTE_SECONDS="${EXPLORER_SESSION_ABSOLUTE_SECONDS:-86400}" # ── Caddy / TLS intent (actual install happens in step 7) ──────── # Collect answers now so the rest of the run has no surprise prompts. diff --git a/tests/test_central_auth_endpoints.py b/tests/test_central_auth_endpoints.py index 0112726..8872348 100644 --- a/tests/test_central_auth_endpoints.py +++ b/tests/test_central_auth_endpoints.py @@ -166,7 +166,7 @@ def test_callback_issues_app_scoped_cookie_for_allowlisted_email( assert "Secure" in cookie assert "SameSite=lax" in cookie assert "Path=/" in cookie - assert "Max-Age=43200" in cookie + assert "Max-Age=86400" in cookie assert "Domain=" not in cookie