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
|
signature-verification surface. Traded deliberately: this is
|
||||||
poll-interval-late rather than real-time, which is judged acceptable for an
|
poll-interval-late rather than real-time, which is judged acceptable for an
|
||||||
HR/offboarding-driven event, not a to-the-second requirement.
|
HR/offboarding-driven event, not a to-the-second requirement.
|
||||||
2. **Also shrink `AUTH_SESSION_HOURS`** as defense in depth, independent of the
|
2. **Revised 2026-09-23, in response to Matt's question about idle time
|
||||||
sync job, so the already-live-session window itself is smaller regardless
|
instead of a flat session length: sessions now slide on activity, with a
|
||||||
of how fast the sync runs. Proposed default: **2 hours**, down from 12 —
|
hard ceiling underneath.** A flat `AUTH_SESSION_HOURS` forces a re-check
|
||||||
flagged as a recommendation, not confirmed: Okta's own SSO session (set on
|
with Okta on a fixed schedule regardless of activity; a pure idle timer
|
||||||
the Okta side, separate from this app's cookie) may make re-authentication
|
with no ceiling does the opposite — a continuously-active session would
|
||||||
silent rather than a real re-login if it outlives this app's shorter
|
never force a fresh Okta check on its own, which is a worse fit for the
|
||||||
session, in which case 2 hours costs little. Confirm against the tenant's
|
exact threat this item exists to address (someone still clicking around
|
||||||
actual Okta session policy before treating 2 as final; if Okta's SSO
|
after being deprovisioned). Decided: **both**.
|
||||||
session is itself long-lived, a shorter number here doesn't hurt, either.
|
- `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
|
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;
|
rehire showing active in Okta again does not automatically restore access;
|
||||||
an admin re-enabling the account is a deliberate act, consistent with
|
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`
|
- **Items:** `D18`
|
||||||
- **Depends on:** nothing (first task, independent of the rest)
|
- **Depends on:** nothing (first task, independent of the rest)
|
||||||
- **Blocks:** nothing
|
- **Blocks:** nothing
|
||||||
- **Surface:** `server/`
|
- **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
|
**Revised 2026-09-23** — originally just "shrink `AUTH_SESSION_HOURS`". Matt
|
||||||
reasoning (defense in depth against an already-live session outliving a
|
asked whether an idle timeout would be a better fit than a flat session
|
||||||
deprovisioning event) in `server/.env.example` next to `AUTH_SESSION_HOURS`,
|
length. It is, but not by itself — see the decision record's reasoning on why
|
||||||
and in `DEPLOYMENT.md`'s auth section. Note in the PR that this is proposed,
|
idle-with-no-ceiling is actually a worse fit for this item's own threat model
|
||||||
not confirmed against the tenant's actual Okta SSO session policy — flag it
|
than a flat expiry would have been. Build both.
|
||||||
for Nick/IT rather than treating 2 as unquestionable.
|
|
||||||
|
**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:**
|
**Done when:**
|
||||||
|
|
||||||
- [ ] default is 2 hours; still overridable via env, unchanged mechanism
|
- [ ] a session with continuous activity stays alive past 30 minutes but is
|
||||||
- [ ] `.env.example` and `DEPLOYMENT.md` explain why
|
cut off at the `AUTH_SESSION_HOURS` ceiling regardless
|
||||||
- [ ] existing session/auth tests updated for the new default where they
|
- [ ] a session with no activity for 30+ minutes is rejected on its next
|
||||||
assumed 12
|
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