feat(api): local-password authentication backend
Add username+password login for Owner/Operator staff accounts on op.console, the primary web login when Zero Trust is not in front of the API. Three handlers form the whole surface: login mints a server-side session cookie, logout revokes it idempotently, and change-password re-verifies the current password before rotating the hash and clearing must_change_password. - Session cookies are HttpOnly+Secure+SameSite=Lax, host-only, stored server-side as a SHA-256 hash with a 12h TTL. - Login is anti-enumeration: every failure runs a uniform bcrypt compare against a dummy hash and returns the same vague error. - Credential-bearing writes require Content-Type: application/json, returning 415 otherwise, to close the cross-site form-POST forgery vector as a belt to the SameSite cookie. - Local auth fails closed: login is rejected unless local_auth_enabled is set, so a Zero-Trust-only deployment never accepts a local password. - Extend the users table with a nullable password_hash and must_change_password; staff are role=admin rows with a hash, players are role=user rows with hash NULL. - /me now reports must_change_password so the panel can force a first-login change. Covered by Go unit tests (handlers, content-type guard, anti-enumeration, forced-change lockdown) and the OpenAPI route-parity gate.
This commit is contained in:
17 files changed
+1370
-17
No files matched your search
@@ -49,6 +49,19 @@ var (
|
||||
errUnauthorized = newError(http.StatusUnauthorized, "unauthorized", "authentication required")
|
||||
errForbidden = newError(http.StatusForbidden, "forbidden", "not permitted")
|
||||
errBadRequest = newError(http.StatusBadRequest, "bad_request", "invalid request")
|
||||
// errInvalidCredentials is the single, deliberately vague answer to any failed
|
||||
// local-password login (spec §B): unknown username, player row, or wrong
|
||||
// password all collapse to it so the response never reveals which usernames
|
||||
// carry a password. The anti-enumeration dummy-hash compare keeps the timing
|
||||
// uniform alongside it (handlers_auth.go).
|
||||
errInvalidCredentials = newError(http.StatusUnauthorized, "invalid_credentials", "invalid username or password")
|
||||
// errPasswordChangeRequired fences a staff principal that still owes a
|
||||
// first-login password change to the change-password surface. The lockdown
|
||||
// middleware returns it from every authenticated route except the opt-out set
|
||||
// (change-password / logout / me), so a half-onboarded account cannot act until
|
||||
// it sets its own password.
|
||||
errPasswordChangeRequired = newError(http.StatusForbidden, "password_change_required",
|
||||
"change your password before continuing")
|
||||
)
|
||||
|
||||
// writeJSON writes v as an indented JSON body with the given status.
|
||||
|
||||
Reference in new issue
Block a user