D18: revise T13.1 to idle timeout + absolute ceiling
Matt asked whether idle time would be a better fit than a flat session length. It is, but idle-alone weakens D18's own purpose - a continuously active session would never force a fresh Okta recheck on its own. Decided: both. AUTH_IDLE_MINUTES (new, default 30) slides the session on activity; AUTH_SESSION_HOURS (existing var, meaning changes to an absolute ceiling, default 12 -> proposed 8) caps it regardless of activity. Docs only in this commit - implementation is T13.1, next.
This commit is contained in:
@@ -259,15 +259,28 @@ the actual code:
|
||||
signature-verification surface. Traded deliberately: this is
|
||||
poll-interval-late rather than real-time, which is judged acceptable for an
|
||||
HR/offboarding-driven event, not a to-the-second requirement.
|
||||
2. **Also shrink `AUTH_SESSION_HOURS`** as defense in depth, independent of the
|
||||
sync job, so the already-live-session window itself is smaller regardless
|
||||
of how fast the sync runs. Proposed default: **2 hours**, down from 12 —
|
||||
flagged as a recommendation, not confirmed: Okta's own SSO session (set on
|
||||
the Okta side, separate from this app's cookie) may make re-authentication
|
||||
silent rather than a real re-login if it outlives this app's shorter
|
||||
session, in which case 2 hours costs little. Confirm against the tenant's
|
||||
actual Okta session policy before treating 2 as final; if Okta's SSO
|
||||
session is itself long-lived, a shorter number here doesn't hurt, either.
|
||||
2. **Revised 2026-09-23, in response to Matt's question about idle time
|
||||
instead of a flat session length: sessions now slide on activity, with a
|
||||
hard ceiling underneath.** A flat `AUTH_SESSION_HOURS` forces a re-check
|
||||
with Okta on a fixed schedule regardless of activity; a pure idle timer
|
||||
with no ceiling does the opposite — a continuously-active session would
|
||||
never force a fresh Okta check on its own, which is a worse fit for the
|
||||
exact threat this item exists to address (someone still clicking around
|
||||
after being deprovisioned). Decided: **both**.
|
||||
- `AUTH_IDLE_MINUTES` (new, default **30**): a session with no request
|
||||
for this long stops being valid. Implemented as a sliding JWT expiry —
|
||||
the token is reissued with a fresh `exp` on activity, throttled so the
|
||||
cookie isn't rewritten on literally every request.
|
||||
- `AUTH_SESSION_HOURS` (existing var, meaning changes to an **absolute
|
||||
ceiling**): no session survives past this many hours from the original
|
||||
sign-in, no matter how continuously active it is. Default changing from
|
||||
12 to a proposed **8** — flagged as a recommendation, not confirmed.
|
||||
- Both defaults, and the mechanism itself, should be sanity-checked
|
||||
against the tenant's actual Okta SSO session policy — if Okta's own
|
||||
session silently outlives either number, re-authentication here is
|
||||
likely a fast redirect, not a real re-login screen, so these numbers
|
||||
cost less than they look like they do. Confirm before treating either
|
||||
as final.
|
||||
3. **The sync only ever disables an account — it never re-enables one.** A
|
||||
rehire showing active in Okta again does not automatically restore access;
|
||||
an admin re-enabling the account is a deliberate act, consistent with
|
||||
|
||||
@@ -11,27 +11,69 @@ important done-when list in this wave — do not relax it to ship faster.**
|
||||
|
||||
---
|
||||
|
||||
### T13.1 — D18: shrink `AUTH_SESSION_HOURS`
|
||||
### T13.1 — D18: idle timeout with an absolute ceiling
|
||||
|
||||
- **Items:** `D18`
|
||||
- **Depends on:** nothing (first task, independent of the rest)
|
||||
- **Blocks:** nothing
|
||||
- **Surface:** `server/`
|
||||
- **Files:** `server/auth.py`, `server/.env.example`, `DEPLOYMENT.md`
|
||||
- **Files:** `server/auth.py`, `server/app.py` (`auth_gate` middleware),
|
||||
`server/.env.example`, `DEPLOYMENT.md`
|
||||
|
||||
**Do:** Change the default from 12 to 2 hours. Document the change and the
|
||||
reasoning (defense in depth against an already-live session outliving a
|
||||
deprovisioning event) in `server/.env.example` next to `AUTH_SESSION_HOURS`,
|
||||
and in `DEPLOYMENT.md`'s auth section. Note in the PR that this is proposed,
|
||||
not confirmed against the tenant's actual Okta SSO session policy — flag it
|
||||
for Nick/IT rather than treating 2 as unquestionable.
|
||||
**Revised 2026-09-23** — originally just "shrink `AUTH_SESSION_HOURS`". Matt
|
||||
asked whether an idle timeout would be a better fit than a flat session
|
||||
length. It is, but not by itself — see the decision record's reasoning on why
|
||||
idle-with-no-ceiling is actually a worse fit for this item's own threat model
|
||||
than a flat expiry would have been. Build both.
|
||||
|
||||
**Do:**
|
||||
|
||||
- Add `login_at` to the JWT payload in `create_token()` — the original
|
||||
sign-in time, distinct from `iat`, which becomes "when THIS token was
|
||||
issued" once tokens start getting reissued. `login_at` never changes across
|
||||
reissues; it's what the absolute ceiling is measured from.
|
||||
- Add `AUTH_IDLE_MINUTES` (default 30). `AUTH_SESSION_HOURS` stays the name
|
||||
for the absolute ceiling, default changing from 12 to a proposed 8 — update
|
||||
its docstring/comment in `auth.py` and `.env.example`, since its MEANING is
|
||||
changing (session length -> hard ceiling on top of a sliding idle window),
|
||||
not just its value.
|
||||
- In `auth_gate` (`server/app.py`), after confirming a request is
|
||||
authenticated: if `now < login_at + AUTH_SESSION_HOURS` (the ceiling hasn't
|
||||
passed), compute `new_exp = min(now + AUTH_IDLE_MINUTES, login_at +
|
||||
AUTH_SESSION_HOURS)`. If `new_exp` is meaningfully later than the current
|
||||
token's `exp` (throttle this — do not reissue on every single request, only
|
||||
when enough time has passed to be worth a new cookie; a few minutes of
|
||||
slack is fine), mint a refreshed token carrying forward `sub`/`username`/
|
||||
`role`/`ver`/`login_at` unchanged, and set it on the response.
|
||||
- If the ceiling HAS passed, do not refresh — let the existing token expire
|
||||
on its own terms (it may already be invalid, or may tick over within the
|
||||
idle window; either way, no new one is issued past the ceiling).
|
||||
- The middleware should not need a DB round trip to do this — everything
|
||||
needed (`sub`, `username`, `role`, `ver`, `login_at`) is already in the
|
||||
validated claims. `get_current_user`'s existing per-request `is_active`/
|
||||
`token_version` check is unaffected and still runs separately.
|
||||
|
||||
**Do not:** reissue the cookie on every request unconditionally — that's a
|
||||
`Set-Cookie` header on every API call for no benefit over a coarser refresh.
|
||||
Do not let the ceiling check silently vanish for tokens issued before this
|
||||
change ships — a token with no `login_at` claim should fall back to treating
|
||||
its own `iat` as `login_at`, not bypass the ceiling entirely.
|
||||
|
||||
**Done when:**
|
||||
|
||||
- [ ] default is 2 hours; still overridable via env, unchanged mechanism
|
||||
- [ ] `.env.example` and `DEPLOYMENT.md` explain why
|
||||
- [ ] existing session/auth tests updated for the new default where they
|
||||
assumed 12
|
||||
- [ ] a session with continuous activity stays alive past 30 minutes but is
|
||||
cut off at the `AUTH_SESSION_HOURS` ceiling regardless
|
||||
- [ ] a session with no activity for 30+ minutes is rejected on its next
|
||||
request
|
||||
- [ ] the cookie is not rewritten on every single request — verify the
|
||||
refresh is throttled, not unconditional
|
||||
- [ ] a pre-existing token with no `login_at` claim (simulating a session
|
||||
issued before this shipped) still gets a hard ceiling, via the `iat`
|
||||
fallback
|
||||
- [ ] `.env.example` and `DEPLOYMENT.md` explain both variables and flag both
|
||||
defaults as proposed, not confirmed against Okta's own session policy
|
||||
- [ ] existing session/auth tests updated for the new mechanism, not just a
|
||||
new number
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user