Files
Spoolman/spoolman/api
Donkie 5ef6401e2b auth: sign-in, first-run setup and account endpoints
Adds GET /auth/config, POST /auth/setup, POST /auth/login, POST /auth/logout,
GET /auth/session and POST /auth/password, plus the Pydantic models behind
them. This is the commit that makes authentication usable.

Every endpoint returns a JSON body rather than a bare 204, because the
client's fetch helpers parse a body unconditionally and an empty response
would surface as a parse failure. The five that bootstrap a session are
allow-listed as public and check for themselves -- requiring a credential to
obtain a credential would be circular.

Anti-enumeration is the part worth reviewing:

- Every failed sign-in returns one identical message, so "no such user" and
  "wrong password" are indistinguishable in the body.
- Unknown, disabled and password-less accounts still spend a full scrypt
  verification via dummy_verify, so they are indistinguishable in timing
  too. Measured: an unknown username costs 47 ms, matching a real verify.
  Skipping it would make a missing account return in about a millisecond and
  hand out a list of valid usernames.
- The locked-account path deliberately does NOT spend a hash. The answer is
  already decided, and burning 50 ms of CPU per attempt against a
  single-process server is a self-inflicted denial of service.

Also corrects a throttling design fault found by testing it. The in-memory
per-username window and the persisted failed_logins/locked_until pair were
two mechanisms for the same job, and the cruder one won: five failures
produced a flat 15 minute lockout from the window, making the graduated
schedule on the user row unreachable dead code. The per-username window is
removed. Per-account throttling is now the persisted lockout alone, which is
graduated and survives a restart, and the per-address window stays for what
the persisted counter cannot see -- guesses sprayed across many usernames,
including ones that do not exist.

Setup resolves a race on the unique username index rather than trusting its
own count check, so two callers reaching an unclaimed instance together
cannot both become owner. Claiming is logged at warning level with the
claiming address.

Verified end to end: config reports setup_required before the first account
and not after; a second claim is rejected with 409; usernames are stored and
matched case-insensitively; cookies come back HttpOnly, SameSite=lax, and
without Secure over plain HTTP; "remember me" adds Max-Age and its absence
does not; a cookie-authenticated POST without the CSRF header is refused and
succeeds with it; sign-out revokes server-side; password change rejects a
wrong current password and the new password works afterwards. Lockout now
follows the intended 60s schedule doubling to a 900s cap, and a correct
password during a lockout is still refused.

The existing integration suite still passes unedited with auth off: 372
tests.
2026-07-28 09:52:03 +02:00
..
2023-04-02 19:19:15 +02:00