Handbooks / Web Security (OWASP) / Chapter 3
Authentication and Sessions
7 pages · ~5 min✓ Reviewed
Builds on Cross-Site Scripting (XSS). Next up: CSRF and CORS.
Password Storage Mental Model
1. Hash, Don't Encrypt
- Store a slow, salted hash, never the password
- Hash = one-way; encryption = reversible
- Never store an encrypted copy either
2. Salt vs Pepper
| Salt | Pepper | |
|---|---|---|
| Value | Random, per user | One app-wide secret |
| Kept | Beside hash, in clear | Env var or KMS |
| Defeats | Rainbow tables, equal hashes | DB leak alone |
- Password
- + salt
- Slow hash
- Store
3. Why Fast Hashes Fail
- MD5, SHA-1, plain SHA-256 are fast
- A GPU tries billions per second
- Use a slow, tunable password hash
Remember
Slow + salted hash in the DB; the pepper lives outside it.
4. Interview Point
- Why not encrypt? Reversible: one key leak exposes every password.
- Salt stops what? Rainbow tables; not brute force on a weak password.
- Salt vs pepper? Salt: per user, stored. Pepper: secret, kept outside the DB.
- SHA-256 alone? Too fast: GPUs try billions per second.
bcrypt and argon2 in Practice
1. bcrypt
- Cost n = 2^n rounds; +1 doubles the time
- Tune to ~250 ms per hash; cost 12 to start
- Salt is generated and embedded by bcrypt
- 72-byte limit: input silently truncated; pre-hash or cap length
2. argon2id
- argon2id is the current default
- Memory-hard: resists GPU/ASIC attacks
- Tuned by memory, iterations, parallelism
- OWASP minimum: 19 MiB, t=2, p=1
3. Usage
- Verify re-derives the hash from the stored salt
javascript
const hash = await argon2.hash(pw); const ok = await argon2.verify(hash, pw); const h2 = await bcrypt.hash(pw, 12); const ok2 = await bcrypt.compare(pw, h2);
Never ==
Compare hashes with verify/compare, not ==: timing leak.
4. Interview Point
- Why argon2id? Memory-hard, so GPUs/ASICs gain little
- Cost factor? 2^n rounds; +1 doubles time
- Upgrade hashes? Rehash on login when params are old
- Salt location? Inside the hash string
Sessions vs JWT
1. Session vs JWT
| Session | JWT | |
|---|---|---|
| Client holds | opaque ID cookie | signed token |
| Server state | yes (shared store, Redis) | none |
| Revoke | instant: delete row | not before exp (denylist) |
| Payload | stays on server | base64, readable |
- JWT =
header.payload.signaturewith claimssub,exp - Payload is encoded, not encrypted: no secrets in it
2. Expiry and revocation
- 1Access tokenabout 15 min
- 2ExpiresAPI returns 401
- 3Refresh tokenrotates on use
- 4New pairold refresh dead
3. Verify every token
- Pin allowed
algorithms; rejectalg=noneand RS256-to-HS256 confusion - Check signature,
exp,iss,aud
javascript
jwt.verify(token, publicKey, {
algorithms: ["RS256"],
issuer: "auth.example.com",
audience: "api",
});exp is checked by default
Logout is not revoke
Deleting the client token does not kill a JWT before exp.
Session Fixation and MFA
1. Session Fixation
- Attacker plants IDknown session ID
- Victim logs inuses that ID
- Server keeps IDnow authenticated
- Attacker shares sessionsame ID
- Fix: regenerate the ID on login and on any privilege change
- Invalidate the old ID
- Never accept IDs from URLs
2. Cookie Hardening
| Setting | Effect |
|---|---|
HttpOnly | No JS access |
Secure | HTTPS only |
SameSite=Lax / Strict | CSRF defence |
| Idle + absolute timeout | Caps a stolen session's life |
3. MFA Factors
Something you know, something you have, something you are.
| Rank | Factor | Catch |
|---|---|---|
| 1 (weakest) | SMS OTP | SIM-swap |
| 2 | TOTP app | 30s codes, shared secret |
| 3 | WebAuthn / passkeys | Phishing-resistant |
4. Interview Point
- Fixation fix? Regenerate the session ID on login.
- HttpOnly? Stops JS reading the cookie (XSS theft), not XSS actions or CSRF.
- Why is SMS weakest? SIM-swap, and codes can be phished.
- Recovery codes? Single-use, stored hashed, shown once; regenerate after use.
Broken Access Control and IDOR
1. Who vs What
- Authentication: who you are
- Authorization: what you may do
- Broken access control (OWASP A01): missing or wrong server-side check
- Hidden buttons and UI checks are not security
2. IDOR and the Fix
- GET /invoices/101
- Change to 102
- No owner check
- Other user's data
- Check ownership server-side on every request
- Deny by default
- Use the session identity, not a client ID
sql
SELECT * FROM invoices WHERE id = ? AND owner_id = session.user_id
no row = 404
3. Escalation and Interview Point
| Example | Test | |
|---|---|---|
| Horizontal | user A reads user B | swap B's ID |
| Vertical | user hits /admin | call admin route as user |
UUIDs are not a fix
Random IDs only obscure the object; the missing check remains.
- IDOR vs escalation? IDOR swaps an object ID; escalation gains a higher role
- Test IDOR? Two accounts: replay A's request with B's ID
- UUIDs enough? No, only hides IDs
- Checks live where? Server, every request
Auth Security Cheat Sheet
1. Passwords
- argon2id or bcrypt cost 12+
- Unique salt per user; optional pepper in KMS
- Constant-time compare, never
== - Rehash on login when parameters change
2. Sessions, JWT, Cookies
| Session | JWT | |
|---|---|---|
| Holds | Opaque ID | Signed claims |
| Revoke | Delete row | Short exp + refresh |
| Verify | Store lookup | Pin alg, check exp |
| Secrets | Server side | Never in payload |
- Cookie: HttpOnly, Secure, SameSite=Lax
- New session ID on login and privilege change (stops fixation)
3. Access Control
- Deny by default
- Check ownership server-side every request
- Use session identity, not client IDs
- MFA for sensitive actions, prefer WebAuthn
IDOR
WHERE id = ? AND owner_id = session.user_id. UUIDs only hide IDs.
4. Interview Point
- Secure login? Rate-limit, verify hash, new session ID, secure cookie, MFA
- Top three mistakes? Fast hashes, unpinned JWT or loose cookies, missing ownership checks
- Audit IDOR? Two accounts: replay A's IDs with B's session; expect 403
Check yourself
A site keeps the same session ID after login. An attacker planted sid=abc in the victim's browser first. What happens?
- The attacker's
sid=abcbecomes the logged-in session. - Fix: regenerate the ID on login and invalidate the old one.
Invoices use random UUIDs, but the query is WHERE id = ?. User A pastes B's invoice UUID. What happens?
- B's invoice is returned: UUIDs hide IDs, they do not authorize.
- Add
AND owner_id = session.user_id.
bcrypt cost goes from 12 to 14. Roughly how does hash time change?
- About 4x longer: cost n means 2^n rounds.
- Old hashes still verify, so rehash on next login.