S081 · AuthN vs AuthZ, Sessions & Password Storage
The two questions every request has to answer.
Module M10: Security · Session 81 of 130 · Track: SYS 🔑
What you'll be able to do after this session
- Explain AuthN vs AuthZ, Sessions & Password Storage to a friend in one minute, out loud, no notes.
- Recognise it when you see it in real code / systems / papers.
- Complete the hands-on exercise at the end without looking up help.
Prerequisites
If you can't answer these in 30 seconds each, redo the linked session first:
Pre-read (skim before the session)
- 🎥 Intuition (5–15 min) · OAuth 2.0 explained simply — Fireship-style 8-min tour of who-are-you vs what-can-you-do.
- 🎥 Deep dive (30–60 min) · OAuth 2.0 and OpenID Connect (in plain English) — Nate Barbettini — the canonical 60-min talk everyone in the field has seen.
- 🎥 Hands-on demo (10–20 min) · How to hash passwords properly with bcrypt (Web Dev Simplified) — code-along that shows salt, cost factor, verify.
- 📖 Canonical article · OWASP Authentication Cheat Sheet — the reference every team copies from.
- 📖 Book chapter / tutorial · MDN — HTTP authentication — how the browser side actually works.
- 📖 Engineering blog · Dropbox — How we migrated password hashing to bcrypt — a real, honest migration story.
(a) Intuition · 5 min
The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.
In one sentence: The two questions every request has to answer.
Imagine a hotel. When you walk up to the front desk and show your passport, the clerk is doing authentication — proving that you are the person on the reservation. When they hand you a key card that opens only floor 12, rooms 1201-1210, they're doing authorization — deciding what you're allowed to do. Same person, two totally separate questions. Every single HTTP request your app receives has to answer both: "who is this?" and "are they allowed?".
The reason we keep them separate is that they change independently. Your identity almost never changes (you're still Dinesh next month), but your permissions change all the time (promoted, moved teams, left the company). If you mash them together — the classic "admin flag on the user row" mistake — you end up with a permissions system that nobody can audit.
Sessions are how the server remembers the answer to "who is this?" without asking you to log in on every click. Two flavours: server-side sessions (the server stores {sessionId → userId} in Redis, browser holds just the opaque cookie) and stateless tokens like JWT (the server stores nothing; the token itself is a signed claim). Both work; they trade different things.
Password storage has exactly one rule you need to carry forever: never store passwords, store slow salted hashes. Use bcrypt / argon2 / scrypt — algorithms that are deliberately slow so a leaked database still takes a hacker centuries per guess. If you ever see SHA-256(password) or, god forbid, plaintext, that's a resume-generating event waiting to happen.
Gotcha to carry forever: AuthN says who, AuthZ says what. Never conflate them, never skip AuthZ because "the UI hides the button" — attackers curl your API directly.
(b) Visual walkthrough · 15 min
Diagrams, worked examples, step-by-step visuals.
Every request flows through the same two checkpoints. Here's the mental model as a sequence diagram:
Worked example 1 — password storage costs. Suppose your DB leaks with 1 M user rows.
| Storage | Time to crack 1 password on RTX 4090 |
|---|---|
| Plaintext | 0 seconds |
| MD5 | ~30 ns (10 billion/sec) |
| SHA-256 | ~100 ns |
| bcrypt cost=12 | ~300 ms — 1 billion× slower than SHA-256 |
| argon2id (default) | ~500 ms |
That factor of a billion is the whole point. Even if the attacker uses 100 GPUs, cracking a single strong password now takes years.
Worked example 2 — session cookie vs JWT trade-off.
| Server session (Redis) | JWT (stateless) | |
|---|---|---|
| Where is state? | Server | Client (in the token) |
| Revoke instantly? | Yes (delete key) | No — must wait for expiry or maintain a denylist |
| Size on wire | ~40 bytes | 400-1000 bytes |
| Works across services? | Needs shared Redis | Any service with the public key |
| Best for | Web apps, admin panels | Microservices, mobile APIs |
Rule of thumb: start with server sessions. Only reach for JWT when you have a real multi-service problem — most apps don't.
(c) Hands-on · 20 min
Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.
Type this into auth_demo.py. It shows bcrypt hashing, verification, and why cost matters.
# pip install bcrypt pyjwt
import bcrypt, time, jwt, secrets, datetime as dt
# ---------- (1) Password storage ----------
password = b"correct horse battery staple"
# Hash with cost=12 (default). Salt is random and embedded in the output.
t0 = time.perf_counter()
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(f"hash took {time.perf_counter()-t0:.3f}s")
print(f"stored: {hashed.decode()}") # $2b$12$<22-char salt><31-char hash>
# Verify — this is what /login does
assert bcrypt.checkpw(password, hashed)
assert not bcrypt.checkpw(b"wrong password", hashed)
# ---------- (2) Show why cost matters ----------
for cost in (4, 8, 12):
t0 = time.perf_counter()
bcrypt.hashpw(password, bcrypt.gensalt(rounds=cost))
print(f"cost={cost:2d} time={time.perf_counter()-t0*1:.3f}s")
# ---------- (3) Session token (server-side style) ----------
SESSIONS = {} # in real life: Redis
def login(user_id: int) -> str:
sid = secrets.token_urlsafe(32)
SESSIONS[sid] = {"user_id": user_id, "created": dt.datetime.utcnow()}
return sid
sid = login(42)
print("session cookie value:", sid)
print("server lookup:", SESSIONS[sid])
# ---------- (4) JWT (stateless style) ----------
SECRET = "do-not-hardcode-in-prod"
token = jwt.encode(
{"sub": 42, "role": "editor",
"exp": dt.datetime.utcnow() + dt.timedelta(minutes=15)},
SECRET, algorithm="HS256")
print("jwt:", token)
print("decoded:", jwt.decode(token, SECRET, algorithms=["HS256"]))
# ---------- (5) AuthZ check ----------
def can(user_role: str, action: str) -> bool:
policy = {"admin": {"*"}, "editor": {"post:write", "post:read"}, "viewer": {"post:read"}}
perms = policy.get(user_role, set())
return "*" in perms or action in perms
print(can("editor", "post:write")) # True
print(can("editor", "admin:delete")) # FalseObserve when you run it:
- The bcrypt hash starts with
$2b$12$— that's the algorithm identifier + cost. - The salt is inside the hash — you never store it separately.
cost=4runs in ~10 ms,cost=12in ~300 ms. That's the security lever.- The JWT is base64 — decode the middle segment on jwt.io and see the claims.
can()is authorization; it happens after we already know who the user is.
Try this modification: add a revoke(sid) function that deletes from SESSIONS, then try to revoke a JWT the same way. You'll discover the fundamental JWT trade-off — you can't, without a denylist.
(d) Production reality · 10 min
How this shows up in real systems, gotchas, war stories, common bugs.
Real war stories the industry has lived through.
The 2012 LinkedIn breach. 6.5 M unsalted SHA-1 password hashes were leaked. Within days, ~90% were cracked because SHA-1 is designed to be fast. The lesson: fast hashes are for file checksums, not passwords. Every serious company migrated to bcrypt/scrypt/argon2 after this. Dropbox published a great writeup of their double-wrapped migration (bcrypt(sha512(pw))) — read it.
The "insecure direct object reference" bug (IDOR — OWASP #1 for years). Backend endpoint GET /api/orders/{id} returns the order without checking whether the requesting user owns it. UI never shows a link to someone else's order, so devs assume it's safe. Attacker just increments the ID. Fix: always re-check ownership in the handler, never trust the UI.
JWT revocation gotcha. Team adopts JWTs with 24-hour expiry. User's laptop gets stolen. Ops team panics — they can't invalidate the token. Now you either (a) drop expiry to 5 min + refresh tokens, (b) maintain a Redis denylist (defeating the "stateless" win), or (c) rotate the signing key (nukes everyone).
Session fixation. Server accepts a session ID from the URL/cookie before login and keeps using it after. Attacker plants a known SID, tricks victim into logging in, then reuses it. Fix: always issue a fresh session ID on successful login.
Timing attacks on password verify. if stored_hash == user_hash: leaks info via response time. Use hmac.compare_digest() or the constant-time compare inside bcrypt.checkpw.
How top teams actually do it. Google, GitHub, Auth0, Okta all separate an identity provider (does AuthN, issues tokens) from resource servers (do AuthZ using those tokens). This is what OIDC and OAuth 2.0 are for. Small teams should not roll their own — use Auth0, Clerk, Supabase Auth, or Cognito. Rolling your own auth is one of the most common ways startups end up in TechCrunch for the wrong reason.
(e) Quiz + exercise · 10 min
- In one sentence each, define AuthN and AuthZ and give a real-world hotel analogy for each.
- Why is bcrypt preferred over SHA-256 for password storage? What single parameter tunes it?
- Name two trade-offs of stateless JWTs vs server-side sessions.
- What is IDOR (insecure direct object reference) and which of the two checkpoints failed?
- Why must a fresh session ID be issued after login (not before)?
Stretch (connects to S059 — OAuth/OIDC): Draw the sequence diagram of an OIDC login where your app is the "client", Google is the "identity provider", and your backend API is the "resource server". Mark exactly where AuthN happens, where AuthZ happens, and which party holds each secret.
Explain-out-loud test
If you can't teach these 3 things to a friend without notes, redo the session:
- What is AuthN vs AuthZ, Sessions & Password Storage? (one sentence, no jargon)
- When would you use it? (one concrete example)
- When would you NOT use it? (one anti-pattern)
What comes next
- Next session (S082): TLS 1.3, PKI & Cert Lifecycle
- Previous (S080): Incident Response — Runbooks, Postmortems, On-Call
- Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.
More from M10 · Security
all modules →