# Login Sessions

Background and reasoning behind design decisions in splam. This page won't tell you how to do something. See the How-To Guides for that.


# Why login doesn't persist across a refresh

Authentication state (`authenticated`, `current_user`, `current_role`) lives in a per-session `reactive.value`, not a signed cookie. Refreshing the browser tab logs you out. This was a deliberate simplicity trade-off: a cookie-based session needs Starlette ASGI middleware and a secret key to sign it, which is more moving parts than a small internal admin tool needs to start. If the app grows beyond a handful of trusted operators, that's the first piece worth revisiting.


# Why the idle timeout is enforced on the server

An unattended browser left on the admin panel is an unattended root console. After `SPLAM_IDLE_TIMEOUT` seconds without activity (15 minutes by default) the session is ended and the login form returns.

The countdown runs in a `reactive.effect` that reschedules itself every 30 seconds with `reactive.invalidate_later`, and activity is recorded server-side by every button press and by changing the service, log length, or log search. A JavaScript timer in the page would have been less code. But anything the browser owns can be disabled by the browser. For the timeout to mean anything, the server has to hold the clock. The 30 second tick is why a logout can land up to 30 seconds after the timeout strictly elapses.

The timeout is recorded in the audit trail as a logout with the result `idle timeout`, which distinguishes it from someone deliberately logging out.

Getting in and staying in are two separate mechanisms, and the diagrams below 1) successful logins, 2) already locked, and 3) failed verification.


## Path 1: Successful Login


*\[Rich HTML output -- view on the documentation site\]*


## Path 2: Already Locked


*\[Rich HTML output -- view on the documentation site\]*


## Path 3: Failed Verification


*\[Rich HTML output -- view on the documentation site\]*


The lockout check comes first, before the password is verified. A locked account is rejected whether or not the password was right, which is what makes the lock worth having.


# Why lockouts live in memory

Five consecutive failed logins lock an account for 15 minutes (`SPLAM_MAX_FAILED_LOGINS`, `SPLAM_LOCKOUT_SECONDS`). The counter is a module-level dict in `auth`, shared across browser sessions but not across restarts.

Persisting it would mean another file on disk that the login path has to write on every failure. That's an unauthenticated write, which is exactly what you don't want on the login path. The audit log already records every failed attempt durably, so the persistent evidence exists either way; what's lost on restart is only the enforcement window. Locking is keyed on the submitted username, so a wrong username locks out just as a wrong password does and the form gives an attacker no signal about which accounts exist.


# Why there are only two roles

`admin` and `auditor`, and nothing in between. The distinction that actually matters for an internal tool is whether an account can change the state of a service. Anything finer (per-service permissions, an approver role) implies a permissions model, a UI to manage it, and a review process: worth building when someone asks for it, not before. Accounts created before roles existed have no `role` field and are read as `admin`, so adding this didn't lock anyone out.
