Index · How it works
7 min readHow it works
Authentication answers "are you who you claim to be?" It is distinct from authorization (A01, "are you allowed to do this?") and from identification (the claim of identity, e.g. a username). A07 failures happen at every stage: registration, login, session issuance, session use, credential recovery, and logout.
Password storage done right
Passwords must never be stored in plaintext or reversible encryption. They must be hashed with a function designed for passwords: slow, salted, and ideally memory-hard.
- Use a password hashing function: Argon2id (current first choice — memory-hard, resists GPU/ASIC cracking), scrypt, or bcrypt. These are deliberately slow and tunable via a work/cost factor.
- Salt: a unique, random value per password, stored alongside the hash. Salts defeat precomputed rainbow tables and ensure two users with the same password get different hashes. Modern PHFs generate and embed the salt automatically.
- Why NOT fast hashes: MD5, SHA-1, SHA-256, and SHA-512 are general-purpose hashes built to be fast. A modern GPU computes billions of SHA-256 hashes per second, so an attacker who steals the database can brute-force or dictionary-attack stolen hashes at enormous speed. Speed is the enemy here.
- Pepper (optional, defense in depth): a secret value not stored in the database (kept in app config / an HSM / a KMS) and mixed into every hash. If only the DB leaks, the pepper still protects hashes. It is a supplement to — never a replacement for — per-password salts.
- Cost tuning: pick parameters so a single hash takes ~250–500 ms on your hardware, and revisit as hardware improves.
❌ BAD — fast, unsalted, trivially cracked from a stolen DB
hash = sha256(password) // billions/sec on a GPU
hash = md5(password) // worse
✅ GOOD — slow, salted, memory-hard (salt + params embedded in the output)
hash = argon2id(password, salt=random16B, memory=64MB, iterations=3, parallelism=4)
// e.g. $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
Credential stuffing & brute force
- Brute force: trying many passwords against one account. Credential stuffing: replaying username/password pairs leaked from other breaches (effective because users reuse passwords).
- Rate limiting: throttle login attempts per IP, per account, and globally. Add exponential backoff.
- Account lockout / soft lockout: lock or slow an account after N failed attempts. Beware that hard lockout enables a denial-of-service against legitimate users — prefer increasing delays, CAPTCHA challenges, or temporary soft locks over permanent lockout.
- Breached-password checks: reject passwords that appear in known-compromised lists. The Have I Been Pwned (HIBP) range API uses k-anonymity — you send the first 5 hex chars of the SHA-1 of the password and get back the suffixes of matching hashes, so the full password never leaves your server.
- Credential-stuffing-specific defenses: device fingerprinting, impossible-travel detection, and MFA blunt stuffing even when the password is correct.
- Password policy: favor length (≥ 8, encourage passphrases of 12+), screen against breached/common passwords, and drop forced periodic rotation and arbitrary composition rules (NIST SP 800-63B guidance) — they push users toward weaker, predictable patterns.
MFA / 2FA
Multi-factor authentication combines factors from different categories: something you know (password), something you have (phone, security key), something you are (biometric).
- TOTP (RFC 6238): time-based one-time codes from an authenticator app (Google Authenticator, Authy). A shared secret seeds a 30-second rotating 6-digit code. Better than SMS but phishable — a user can be tricked into typing the code into a fake site.
- SMS OTP: weakest common second factor — vulnerable to SIM swapping, SS7 interception, and phishing. Acceptable as a fallback, not a primary.
- WebAuthn / passkeys (FIDO2): phishing-resistant public-key authentication. The browser/authenticator signs a challenge with a private key bound to the origin; the private key never leaves the device, and the origin binding means a phishing site cannot relay the credential. Passkeys are discoverable WebAuthn credentials, often synced across a user's devices — the current best practice and a path to passwordless.
- Layering: require MFA for sensitive actions (step-up auth) even within an authenticated session.
Session management
After login, the server issues a session that represents the authenticated user across stateless HTTP requests.
- Session IDs must be long, cryptographically random (high entropy, ≥ 128 bits), and generated by the framework's session manager — never a sequential or guessable value.
- Cookie flags:
Secure— only sent over HTTPS.HttpOnly— not readable from JavaScript, mitigating session theft via XSS.SameSite=StrictorLax— limits cross-site sending, mitigating CSRF.
❌ BAD — readable by JS, sent over HTTP, sent cross-site
Set-Cookie: session=abc123
✅ GOOD
Set-Cookie: session=<128-bit-random>; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600
- Session fixation: an attacker sets/knows a victim's session ID before login, then rides the now-authenticated session. Fix: regenerate (rotate) the session ID on every login and on any privilege escalation. Never accept a session ID supplied via the URL.
- Rotation on login: always issue a fresh session identifier when authentication state changes.
- Timeouts: enforce both an idle timeout (expire after inactivity, e.g. 15–30 min for sensitive apps) and an absolute timeout (hard cap on total session life regardless of activity, e.g. 8–12 hours).
- Logout: must invalidate the session server-side, not merely delete the client cookie. Provide "log out of all devices" for credential-change events.
JWT pitfalls
JSON Web Tokens are popular for stateless auth but have sharp edges.
alg: none: the spec allows an unsigned token. If the server honorsnone, an attacker strips the signature and forges any claims. Always pin the expected algorithm server-side; never trust the token's ownalgheader to choose the verification method.- Algorithm confusion (RS256 → HS256): if a server verifies with a generic "verify with this key" call, an attacker can switch an RS256 token to HS256 and sign it using the public key as the HMAC secret. Pin the algorithm.
- Weak secret: HS256 with a guessable/short secret is brute-forceable offline. Use a long, random secret or asymmetric keys.
- No expiry / long expiry: always set a short
exp. Stolen long-lived tokens are valid until they expire. - Storage —
localStoragevs cookie:localStorageis readable by any JS, so an XSS instantly exfiltrates the token. Prefer anHttpOnly; Secure; SameSitecookie for the session/refresh token. If you must use bearer tokens in JS, keep them short-lived and in memory. - Revocation: JWTs are self-contained and valid until
exp— you cannot trivially revoke one. Mitigate with short-lived access tokens + a server-side refresh-token store you can revoke, or a deny-list of token IDs (jti).
Password reset flows done safely
- Generate a single-use, high-entropy, time-limited reset token; store only a hash of it server-side.
- Deliver it out-of-band (email/SMS) and never reveal whether the address exists — show the same "if an account exists, we've sent a link" message regardless.
- Expire the token quickly (e.g. 15–60 min), invalidate it after use, and invalidate all other active sessions on password change.
- Don't use security questions as the sole recovery factor (answers are guessable/discoverable). Consider re-prompting for MFA during reset.
OAuth 2.0 / OIDC basics
OAuth 2.0 is an authorization framework (delegated access — "let this app read my calendar"). OpenID Connect (OIDC) layers authentication on top (an id_token proving who the user is). For "log in with Google/Microsoft", you want OIDC.
- Authorization Code flow + PKCE: the standard, secure flow for web and mobile/SPA clients. The client gets a short-lived authorization code via the browser, then exchanges it server-side (or with PKCE) for tokens. PKCE (Proof Key for Code Exchange) binds the code to the client that started the flow, defeating code interception — now recommended for all client types, public and confidential.
- Avoid the Implicit flow: it returned tokens directly in the URL fragment and is deprecated; use Authorization Code + PKCE instead.
- Validate everything: verify the
id_tokensignature,iss,aud,exp, and thestate(CSRF protection) andnonce(replay protection) you sent. - Don't roll your own: federated identity is full of subtle pitfalls. Use a vetted identity provider / library (Keycloak, Auth0, Okta, certified OIDC libraries).
CAPTCHA and enumeration
- Generic error messages: return the same response for "no such user" and "wrong password" (e.g. "Invalid username or password"). Differing messages, HTTP status codes, or response timing let an attacker enumerate valid accounts — which then feeds targeted brute force and stuffing.
- Enumeration also leaks at registration ("email already taken"), password reset, and login; close all of them with uniform responses.
- CAPTCHA: presents a human-verification challenge after suspicious activity (failed attempts, automation signatures) to slow bots and credential stuffing. Use as a friction layer, not a sole control.