mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-23 02:22:20 -05:00
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.