Password authentication
Implement login with generic errors
Compare password hashes safely, avoid user enumeration, rotate session state, and preserve consistent observable behavior.
8 minute lesson
Login receives an identifier and secret, finds a verifier, and establishes a new authenticated session only after a successful comparison.
Use the library’s verify function and a dummy verifier path for unknown accounts when appropriate. Return the same public error for an unknown account and wrong password. On success, create a fresh session identifier and do not accept a client-supplied pre-login session as authenticated.
Keep the visible contract boring:
{ "error": "Invalid email or password" }
The status, response body, redirect, and broad timing behavior should not reveal whether the account exists, is disabled, or used the wrong password. Internal logs may record a safe reason code, but never the password or session token.
Handle failures in this order:
- Validate input shape and length.
- Apply cheap request-size, network, and global abuse limits.
- Load the account or a dummy verifier.
- Run the password library’s comparison.
- Apply account-specific policy without changing the public outcome.
- Create a new session on success.
The early limits protect CPU and memory from requests that should never reach an expensive password hash. Account-specific throttling still happens after a uniform real-or-dummy verification path so the response does not become an account-existence oracle.
Do not reuse an anonymous session identifier after login. Rotation prevents an attacker who supplied or learned the earlier value from inheriting the authenticated session.
A database or hashing-service failure is not “invalid credentials.” Return a generic temporary failure and alert internally so an outage does not look like thousands of bad passwords.
Test success, unknown email, wrong password, disabled account, storage failure, and a client-supplied pre-login cookie. Only success may create a fresh session.
Lesson completed