Authentication and sessions
How users prove who they are, and how you keep them signed in without handing an attacker the keys. This is the most attacked surface of any app.
Checklist
- Hash passwords with a slow, salted algorithm. Use bcrypt, scrypt, or Argon2, never plain SHA or MD5, and never store plaintext. If you can read a user's password, so can an attacker with your database.
- Throttle login and reset. Cap attempts per IP and per account (for example ten per fifteen minutes), then return 429. Without this, brute force is trivial.
- Do not leak which emails exist. Login, signup, and password reset should return the same generic response whether or not the account exists, with consistent timing and status codes.
- Make reset and verification tokens short-lived and single-use. One hour or less, invalidated on use. Treat them like passwords.
- Bind the OAuth flow to the browser that started it. If you sign in with
Google, GitHub, or similar, store a
statevalue in a cookie on the start request and compare it on the callback. Without this binding an attacker can send a victim a link that logs the victim into the attacker's account (login-CSRF). See APIs and requests. - Validate the identity provider's response. Require a verified email, check the token audience and issuer, and never trust an id the client supplies.
- If you offer multi-factor auth, gate step two behind step one. The second factor must only be reachable after the password check passes, with a one-time challenge, or the code space is brute-forceable on its own.
- Expire sessions and support revocation. Set a sane token lifetime, and make a password change invalidate all existing sessions.
- Check your login against a breached-password list (for example the HIBP k-anonymity API) at registration and reset.
How to test
- Script fifty wrong logins and confirm you get throttled.
- Compare the response to a real email versus a fake one on reset. They should be identical.
- Start an OAuth login in one browser, complete it in another. It should fail.