Two questions, never one
- Authentication (AuthN): who are you? — verifying identity. Failure → 401.
- Authorization (AuthZ): what may you do? — verifying permission. Failure → 403.
Conflating them is both a vocabulary failure in interviews and a bug class in code ("logged in" ≠ "allowed"). Every request answers both, in that order — and always on the server.
Passwords: storage is the whole game
Rule zero: you never store passwords — you store proof you could recognize one.
# registration
hash = bcrypt.hashpw(password, bcrypt.gensalt()) # store THIS, never the password
# login
ok = bcrypt.checkpw(attempt, stored_hash) # re-hash and compare
Why each piece exists:
- Hashing (one-way — hash functions, weaponized): a database leak reveals hashes, not passwords.
- Salt (random per-user value mixed in): identical passwords get
different hashes, killing precomputed "rainbow table" lookups and
exposing nothing when two users share
hunter2. - Slow by design: bcrypt/argon2 are deliberately expensive (~100 ms) so offline brute-force costs attackers centuries, not afternoons. Fast hashes (SHA-256, MD5) are disqualifying answers for passwords — speed is the vulnerability.
The real problem: staying logged in
HTTP is stateless (Level 0) — every request arrives a stranger. Logging in once and being remembered requires the client to present something on every request. Two architectures:
Sessions — the server remembers
Login → server creates a record (session_id → user, expiry) in
Redis/DB → sends the id in an httpOnly cookie → every request carries
the cookie → server looks the session up.
- ✅ Revocation is trivial — delete the record; logout/ban is instant.
- ✅ The token itself is meaningless — nothing to decode if stolen from the database.
- ❌ Every request costs a session-store lookup; the store must scale with you (a hash table at datacenter scale).
JWT — the client carries the proof
A JWT (JSON Web Token) is a signed claim document:
header.payload.signature (base64). The payload carries claims
({"sub": 42, "role": "user", "exp": 1718000000}); the signature —
computed with the server's secret — makes it tamper-evident: alter one
claim and verification fails. Login → server signs a token → client
sends Authorization: Bearer <jwt> → server verifies the signature
and trusts the claims, zero lookups.
- ✅ Stateless — any server (or microservice) verifies independently; nothing to look up, nothing to replicate.
- ❌ Revocation is the famous weakness — a signed token is valid
until
exp, period. Ban a user and their token keeps working. - ⚠️ Signed ≠ encrypted — anyone can read the payload (base64-decode it live and lose your innocence); it only can't be modified. No secrets in claims, ever.
The production pattern: both
Short-lived access JWT (5–15 min) + long-lived refresh token (stored server-side, revocable). API calls verify the JWT statelessly; every few minutes the client redeems the refresh token for a fresh JWT — that exchange hits the database, making it the revocation checkpoint. Ban a user → their access dies within minutes, with stateless verification 99% of the time. This compromise answers the interview question "JWT or sessions?" — both, at different timescales.
Where the client keeps it: httpOnly, Secure cookie beats localStorage (any injected script can read localStorage — XSS = stolen token; httpOnly is invisible to JS). Cookies need CSRF defenses (SameSite + tokens); the full XSS/CSRF story is in security.
OAuth 2.0 — "Login with Google"
OAuth answers a different question: delegation — letting users prove identity (or grant access to their data elsewhere) without giving your app their password.
Why the code dance instead of handing the token through the browser:
the redirect travels through URLs (logged, leaked, shoulder-surfed) — so
the browser only ever carries a worthless one-time code; the real token
exchange happens server-to-server, authenticated by client_secret,
which the browser never sees. (state blocks CSRF on the redirect;
OIDC — OpenID Connect — is the thin identity layer on top that
standardizes "who is this user" as a JWT id_token.)
After OAuth completes, you're back to the previous section: issue your own session/JWT. OAuth authenticates; it doesn't replace your auth architecture.
Authorization: RBAC
The 80% answer: roles on the identity (user, support, admin),
permission checks at the boundary —
FastAPI's require_admin dependency, Spring
Security's @PreAuthorize, Express middleware. Two rules carry the
interview:
- Check ownership, not just role.
GET /orders/42must verify the order belongs to the requester — forgetting this is IDOR (insecure direct object reference), the most common real-world API vulnerability: enumerate ids, read strangers' data. - Deny by default. New endpoints require auth unless explicitly public, not vice versa.
Common mistakes
- Fast hashes or — career-ending — plaintext passwords.
- Secrets in JWT payloads (it's readable!) or tokens in localStorage.
- No token expiry, or 30-day access tokens "for convenience" — convenience for attackers too.
- Building login forms before knowing OAuth exists — for most apps, "login with Google + email magic link" outships a password system and deletes the breach risk.
- 401/403 confusion — in code and in answers: not-identified vs identified-but-forbidden.
- Rolling your own crypto — use bcrypt/argon2, a maintained JWT library, and an OAuth library; the bugs you'd write are the ones attackers scan for.
Interview perspective
Practice
- Hash reps: register/login against a dict store with bcrypt; verify two users with the same password store different hashes; time 1,000 verifications and explain why that slowness is a feature.
- JWT from scratch (once, to demystify): HMAC-sign
base64(header).base64(payload)yourself; tamper with one payload byte and watch verification fail; then base64-decode a real JWT from any site and inventory its claims. - Build the hybrid: add 10-minute access JWTs + refresh-token rotation to your FastAPI/Express API; demonstrate that banning a user locks them out within one refresh window.
- Break an IDOR: make
GET /orders/{id}skip the ownership check, fetch another user's order as user A — then fix it and write the test that would have caught it.
Next: WebSockets — when request/response itself is the wrong shape.