πŸ”

Authentication & Session Security

A Complete 10-Chapter Course

Topics covered:
AuthN vs AuthZ & threat model Β· Password hashing Β· Login defences Β· Sessions & cookies
Session security Β· MFA & passkeys Β· Tokens & JWTs Β· OAuth 2.0 / OIDC
Account recovery Β· Testing & hardening checklist

Exercises: 30 hands-on exercises with worked solutions
Format: A4 Β· Dark-theme code examples Β· completes the HTTPS / CSRF / XSS quartet

Table of Contents

  1. Foundations: AuthN vs AuthZ & the Threat Model
  2. Password Storage Done Right
  3. Login Attacks & Defences
  4. Sessions & Cookies
  5. Session Security
  6. Multi-Factor Authentication
  7. Token-Based Auth & JWTs
  8. OAuth 2.0 & OpenID Connect
  9. Account Recovery & Verification
  10. Testing, Pitfalls & Hardening Checklist
Chapter 1 of 10

Foundations: AuthN vs AuthZ & the Threat Model

Chapter 1
Foundations: AuthN vs AuthZ & the Threat Model
What identity, authentication, and authorization actually mean β€” and what attackers want

Across the HTTPS, CSRF, and XSS courses, one phrase kept recurring: "the session," "the credential," "riding the user's authority." This course is the foundation underneath all of them β€” how a system establishes who you are and keeps that fact secure over time. Chapter 1 sets the vocabulary precisely (the most-confused pair in security: authentication vs authorization) and frames the threat model the rest of the course defends against.

Authentication vs Authorization β€” The Core Distinction

These two are constantly conflated, abbreviated confusingly (AuthN and AuthZ), and even share the prefix "auth" β€” but they answer entirely different questions:

Authentication (AuthN)Authorization (AuthZ)
QuestionWho are you?What are you allowed to do?
Verifiesyour identity (you are who you claim)your permissions (you may access X)
Happensfirst β€” at loginafter β€” on each protected action
Exampleentering a correct password / passkeybeing allowed into the admin panel
Failure looks like"Login failed" (401 Unauthorized)"Access denied" (403 Forbidden)
The order is fixed β€” and the HTTP status codes are (confusingly) named backwards
Authentication always comes before authorization: you can't decide what someone may do until you know who they are. A useful mnemonic: AutheNtication = who (ideNtity); AuthoRization = Rights. One historical wrinkle to know: HTTP 401 "Unauthorized" actually means un-authenticated (you haven't proven who you are), while 403 "Forbidden" means authenticated-but-not-authorized. The 401 name is a 30-year-old misnomer baked into the spec β€” expect it.

This course focuses on authentication and the session that follows it β€” establishing identity and keeping that established identity secure. (Authorization β€” roles, permissions, access control β€” is a large topic in its own right; we touch it only where the two interact.)

The Three Factors of Authentication

Authentication works by checking one or more factors β€” categories of evidence that you are who you claim. There are three classic types:

Factor"Something you…"Examples
Knowledgeknowpassword, PIN, security question
Possessionhavephone (TOTP app), security key, passkey
Inherencearefingerprint, face, biometric

Multi-factor authentication (MFA) means combining factors from different categories β€” e.g. a password (know) plus a phone code (have). Two passwords aren't MFA; they're the same factor twice. The power of MFA is that an attacker must compromise independent channels β€” knowing your password is useless without also having your phone (Chapter 6 goes deep).

The Wider Picture: Identity, AuthN, AuthZ, Accountability

Authentication sits in a chain of four related concepts. Keeping them distinct clarifies a lot of security design:

  • Identification β€” claiming an identity ("I'm user philip"). Just a claim, unverified.
  • Authentication β€” proving that claim (the password/passkey checks out).
  • Authorization β€” granting access based on the proven identity ("philip may edit posts").
  • Accountability β€” logging what the identity did, so actions can be traced (audit logs).

The Threat Model: What Attackers Want

Auth security is best understood by what an attacker is trying to achieve. The overarching goal is account takeover β€” operating as a legitimate user β€” and there are several routes to it:

Attacker goalHowCourse chapter
Steal the credentialphishing, breach reuse, weak password storage2, 3
Guess the credentialbrute force, credential stuffing, spraying3
Steal the sessionsession hijacking, XSS, fixation4, 5
Bypass MFASMS interception, MFA fatigue, phishing proxies6
Abuse tokensJWT flaws, stolen refresh tokens, OAuth misconfig7, 8
Hijack recoveryweak password-reset / email-verification flows9

Where This Fits With the Other Three Courses

This course completes a quartet β€” each protects a different part of the same login-and-session lifecycle:

CourseProtectsAgainst
HTTPS / TLSthe credential/session in transiteavesdropping, tampering on the wire
Authentication (this)establishing & holding the identitycredential theft, session hijacking, weak MFA
CSRFagainst abuse of an active sessionforged requests riding the session
XSSthe page that holds the sessionscript stealing/riding the session
The courses interlock β€” a weakness in one undermines the others
These aren't independent. HTTPS without strong authentication protects a worthless credential; strong authentication over plain HTTP leaks the credential on the wire; perfect auth with an XSS hole lets a script steal the session anyway (XSS course: "fix XSS first"); and CSRF rides whatever session your authentication established. The recurring cookie-flag advice β€” HttpOnly, Secure, SameSite β€” that appeared in all three previous courses lives here, on the session cookie, and Chapter 5 finally ties it together. Account security is only as strong as the weakest link across all four.

Hands-On Exercises

Exercise 1

Classify each as Authentication or Authorization: (a) the site rejects your password; (b) you're logged in but can't open /admin; (c) a fingerprint unlocks your phone; (d) a regular user is blocked from deleting another user's post. For each, give the likely HTTP status (401 vs 403) and justify it.

πŸ“„ View solution
Exercise 2

For each combination, state whether it's true multi-factor authentication and why: (a) password + SMS code; (b) password + security question; (c) fingerprint + passkey on a hardware key; (d) PIN + password. Name the factor category of each element.

πŸ“„ View solution
Exercise 3

For each prior-course attack, state which authentication threat-model goal it serves and why fixing it matters to account security: (a) XSS stealing document.cookie; (b) a CSRF forged "change email"; (c) TLS stripping on a login page. Tie each to "account takeover."

πŸ“„ View solution

Chapter 1 Quick Reference

  • Authentication (AuthN) = "who are you?" (verify identity); Authorization (AuthZ) = "what may you do?" (verify rights)
  • AuthN always comes first; mnemonic: autheNtication = ideNtity, authoRization = Rights
  • HTTP 401 "Unauthorized" really means un-authenticated; 403 "Forbidden" means authenticated-but-not-authorized (spec misnomer)
  • Three factors: knowledge (know) Β· possession (have) Β· inherence (are); MFA = factors from different categories
  • Full chain: identification β†’ authentication β†’ authorization β†’ accountability
  • Threat model: the goal is account takeover β€” via stolen/guessed credentials, stolen sessions, bypassed MFA, abused tokens, or hijacked recovery
  • This course completes the quartet (HTTPS / Auth / CSRF / XSS) β€” they interlock; the weakest link sets account security
  • Next chapter: password storage done right β€” hashing vs encryption, salts, and slow hashes (bcrypt/scrypt/Argon2)
Chapter 2 of 10

Password Storage Done Right

Chapter 2
Password Storage Done Right
Hashing vs encryption, salts, peppers, and slow hashes (bcrypt / scrypt / Argon2)

The single highest-impact decision in authentication is how you store passwords β€” because databases get breached, and when one does, the difference between "annoying" and "catastrophic" is entirely down to this. The rule is simple to state and surprisingly often gotten wrong: never store passwords; store a slow, salted hash of them. This chapter explains every word of that.

Rule Zero: Never Store the Password Itself

You do not need to know a user's password β€” you only need to verify that someone presenting a password knows the same one. That means you never store the password (plaintext), and you never store something reversible back to the password either. When a user logs in, you apply the same one-way transform to what they typed and compare the results.

Plaintext and "encrypted" passwords are both wrong β€” and people confuse the second for safe
Storing plaintext is obvious negligence. The subtler mistake is encrypting passwords β€” which sounds secure but isn't appropriate here. Encryption is reversible: it needs a key, and whoever has the key (an attacker who breaches the server, a malicious insider) can decrypt every password back to plaintext. Passwords should be hashed, not encrypted, precisely because hashing is one-way β€” there's no key to steal and no way back. "We encrypt passwords" in a post-breach statement is a red flag, not reassurance.

Hashing vs Encryption

HashingEncryption
Directionone-way (irreversible)two-way (reversible with key)
Needs a key?noyes β€” and the key can be stolen
Verify a password byhashing input, comparing hashes(would require decrypting β€” wrong model)
Right for passwords?YesNo

But Not Just Any Hash: Fast Hashes Are the Wrong Tool

Reaching for SHA-256 or (worse) MD5 seems reasonable β€” they're one-way. The problem: those are fast hashes, designed to digest data quickly. For passwords that speed is a liability, because it lets an attacker who steals the hashes try billions of guesses per second on a GPU.

// WRONG β€” a fast general-purpose hash const hash = sha256(password); // GPUs try billions/sec against this // RIGHT β€” a deliberately slow password hash (bcrypt) const hash = await bcrypt.hash(password, 12); // tuned to be slow

Two Attacks Fast Hashes Enable

  • Brute force / dictionary attacks β€” try every common password (and variations) against a stolen hash. At billions/sec, weak passwords fall in seconds.
  • Rainbow tables β€” giant precomputed tables mapping common-password hashes back to the password, so the attacker doesn't even compute β€” they just look up. This works because the same password always produces the same hash. Salting defeats this.

Salt: Defeating Precomputation & Identical-Password Leakage

A salt is a unique, random value generated per user and combined with the password before hashing. It's stored alongside the hash (it's not secret). Two effects, both crucial:

// each user gets a unique random salt; stored with the hash salt = randomBytes(16) stored = hash(salt + password) + ":" + salt
  • Defeats rainbow tables β€” precomputed tables are useless because the attacker would need a separate table for every possible salt. They're forced to attack each hash individually.
  • Hides identical passwords β€” two users with the same password get different hashes (different salts), so a breach doesn't reveal "these 500 accounts all use password123."
Good news: modern password hashes salt for you
You rarely manage salts by hand. bcrypt, scrypt, and Argon2 generate a random salt automatically and embed it in the output string β€” the hash itself contains the algorithm, cost parameters, salt, and digest, all in one self-describing value. So bcrypt.hash(pw, 12) already produces a uniquely-salted hash, and bcrypt.compare(pw, stored) reads the salt back out of the stored string to verify. You don't store the salt separately β€” it's in there.

Slow Hashes: bcrypt, scrypt, Argon2

The real defence is a password hashing function deliberately engineered to be slow and resource-intensive, with a tunable cost you raise as hardware gets faster:

AlgorithmCost is…Notes
bcryptCPU (work factor / rounds)battle-tested, ubiquitous, safe default; ~72-byte input limit
scryptCPU + memorymemory-hard β€” resists GPU/ASIC better than bcrypt
Argon2CPU + memory + parallelismmodern winner (PHC 2015); Argon2id is today's recommended choice

The cost factor is the key tunable: bcrypt's work factor of 12 means 2ΒΉΒ² internal iterations. Each +1 doubles the time. You set it so a single hash takes a noticeable fraction of a second on your server β€” trivial for one legitimate login, but devastating to an attacker trying billions of guesses. As hardware improves, you raise it. Memory-hardness (scrypt/Argon2) goes further: requiring lots of RAM per guess neutralizes the massive parallelism of GPUs and custom cracking hardware.

Pepper β€” an optional extra layer
A pepper is a secret value added to the password before hashing β€” like a salt, but the same for all users and not stored in the database (kept in the app config / a secrets manager / an HSM). The idea: if only the database is breached (but not the app secrets), the attacker is missing the pepper and can't crack the hashes at all. It's a defence-in-depth bonus, not a substitute for a proper salted slow hash β€” and it adds key-management complexity, so treat it as optional hardening.

Don't Roll Your Own β€” and Verify in Constant Time

Use a vetted library (bcrypt, argon2 for Node; your framework's built-in). Two operational notes: the library's compare/verify function does a constant-time comparison (to avoid timing leaks), and you should plan to upgrade hashes over time β€” on successful login, if the stored hash used an old algorithm or low cost, re-hash the password with the current parameters and save it.

Hands-On Exercises

Exercise 1

Explain why passwords must be hashed, not encrypted, and why even a one-way hash like SHA-256 is the wrong choice for passwords. Then implement correct storage and verification with bcrypt in Node (hash on signup, compare on login).

πŸ“„ View solution
Exercise 2

Explain how a salt defeats rainbow tables and hides identical passwords, using two users who both chose hunter2. Then explain why a salt alone is not enough and a slow/cost factor is still required.

πŸ“„ View solution
Exercise 3

A breached database is found to store passwords as unsalted MD5. Explain, step by step, how an attacker cracks the bulk of these in minutes, and write the remediation plan (algorithm choice, cost, salting, forced resets, upgrade-on-login). Compare bcrypt/scrypt/Argon2 and recommend one.

πŸ“„ View solution

Chapter 2 Quick Reference

  • Never store the password β€” store a slow, salted, one-way hash; verify by hashing the input and comparing
  • Hash, don't encrypt β€” encryption is reversible (key can be stolen); hashing is one-way, no key
  • Fast hashes (MD5/SHA-256) are wrong for passwords β€” GPUs try billions of guesses/sec
  • Salt = unique random per-user value (stored, not secret) β€” defeats rainbow tables & hides identical passwords
  • Modern password hashes generate & embed the salt automatically in their output string
  • Slow hashes: bcrypt (CPU), scrypt (CPU+memory), Argon2id (modern recommended) β€” with a tunable cost factor
  • Cost factor β€” set so one hash takes a noticeable fraction of a second; raise it as hardware improves; memory-hardness beats GPUs
  • Pepper = optional app-side secret (not in DB) for defence in depth; don't roll your own; upgrade hashes on login
  • Next chapter: login attacks & defences β€” brute force, credential stuffing, rate limiting, lockouts
Chapter 3 of 10

Login Attacks & Defences

Chapter 3
Login Attacks & Defences
Brute force, credential stuffing, password spraying β€” and how to blunt them

Chapter 2 made stolen hashes hard to crack. This chapter defends the live login endpoint β€” where attackers don't need your database, they just guess against your form. The attacks differ in how they guess, and the defences differ accordingly, but they share a theme: make guessing slow, costly, and visible.

The Family of Guessing Attacks

AttackHow it guessesTargets
Brute forcetries many passwords against one accounta single targeted user
Dictionarytries common passwords / wordlists (not all combinations)a single user, efficiently
Credential stuffingreplays real username+password pairs from other breachesmany accounts, reused passwords
Password sprayingtries a few common passwords across many accountsmany users; evades per-account lockout
Credential stuffing is the big one today β€” and it's why password reuse is dangerous
The most common real-world account-takeover attack isn't classic brute force β€” it's credential stuffing. Attackers take the billions of email:password pairs leaked in past breaches and replay them against your login, betting that people reuse passwords. They're not guessing β€” they're using known-correct credentials from elsewhere, so a meaningful percentage just work. This is why "use a unique password per site" matters, why breach-password checks (below) help, and why MFA (Chapter 6) is the real backstop: a stuffed password alone shouldn't be enough.

Defence 1: Rate Limiting

The first line of defence is limiting how many login attempts a given source can make in a window. Track attempts and reject once a threshold is exceeded:

// conceptual: cap attempts per IP + per account over a time window if (attempts(ip) > 10 in last 10 min) return 429; // Too Many Requests if (attempts(account) > 5 in last 15 min) slowDown();

Rate limit on multiple keys β€” by IP, by account, and ideally combinations β€” because each alone has a gap: per-account limits don't stop spraying (one try per account across thousands), and per-IP limits don't stop a botnet (one try per IP across thousands of IPs). Pair coarse IP limits with per-account limits to cover both.

Defence 2: Account Lockout β€” Carefully

Locking an account after N failed attempts is classic, but it's a double-edged sword:

Naive account lockout creates a denial-of-service against your own users
If you hard-lock an account after, say, 5 failures, an attacker can deliberately lock out any user by submitting wrong passwords for them β€” a trivial denial-of-service, and a nuisance-weapon against specific people. It also doesn't stop password spraying (which makes only 1–2 attempts per account). Better approaches: exponential backoff (each failure increases the delay before the next attempt is accepted) and temporary, self-clearing lockouts rather than permanent ones, combined with step-up friction (CAPTCHA / MFA challenge) on suspicious activity instead of a hard block. The goal is to slow attackers without handing them an easy way to lock out legitimate users.

Defence 3: Don't Leak Which Part Was Wrong (User Enumeration)

A subtle but important defence: your login (and signup, and password-reset) responses must not reveal whether a username exists. Compare:

// LEAKY β€” different messages reveal valid usernames "No account with that email" // -> email is NOT registered "Incorrect password" // -> email IS registered (enumeration!) // SAFE β€” identical generic message either way "Invalid email or password"

Distinct messages let an attacker enumerate valid accounts (which then makes credential stuffing and spraying far more efficient β€” they only target real accounts). Beyond the message text, watch for timing leaks: if a valid username runs the slow password hash but an invalid one returns instantly, the response-time difference reveals validity. Mitigate by running a dummy hash comparison even when the user doesn't exist, so both paths take similar time. The same generic-response discipline applies to signup ("if that email is new, we've sent a link") and password reset ("if that account exists, we've emailed instructions").

Defence 4: CAPTCHA & Step-Up Friction

Introduce friction that's cheap for humans but expensive for bots β€” after signs of abuse rather than on every login (to avoid harming UX). A CAPTCHA (or a modern proof-of-work / risk-based challenge) triggered after a few failures, or for traffic from suspicious IP ranges, sharply raises the cost of automated guessing. Risk-based authentication goes further: challenge harder when the login looks unusual (new device, new country, impossible travel) and stay frictionless when it looks normal.

Defence 5: Breached-Password & Strength Checks

Stop bad passwords at the source. Modern guidance (NIST) favours checking new passwords against breach corpora rather than imposing arbitrary composition rules:

  • Block known-breached passwords β€” services like Have I Been Pwned's k-anonymity API let you reject passwords that appear in breach datasets without sending the password itself. This directly defangs credential stuffing.
  • Encourage length over complexity β€” long passphrases beat short "P@ssw0rd!"-style strings; the old "must have an uppercase, number, and symbol" rules push users toward predictable patterns.
  • Don't force frequent rotation β€” mandatory periodic changes lead to weaker, incremental passwords (Spring2024! β†’ Summer2024!); rotate only on suspected compromise.
The honest hierarchy: MFA outranks all of these
Rate limiting, lockouts, enumeration-hardening, and CAPTCHAs all raise the cost of guessing, but a sufficiently determined or distributed attacker (botnet + leaked credentials) can still grind through. The defence that actually changes the game is multi-factor authentication (Chapter 6): even a correct, stuffed password fails without the second factor. Treat the defences in this chapter as essential friction that protects single-factor accounts and buys time β€” but push users toward MFA, which makes a guessed/stuffed password insufficient on its own.

Hands-On Exercises

Exercise 1

Distinguish brute force, credential stuffing, and password spraying β€” how each guesses and why per-account lockout stops one but not the others. Then explain why rate limiting must key on both IP and account.

πŸ“„ View solution
Exercise 2

Show how a login form leaks valid usernames via (a) different error messages and (b) response timing. Write the fixes for both (generic message + dummy hash), and explain why the same discipline must extend to signup and password-reset responses.

πŸ“„ View solution
Exercise 3

Explain why naive "lock after 5 failures" account lockout can be turned into a denial-of-service and still fails to stop spraying. Design a better defence layering (backoff, temporary locks, IP+account rate limits, breach checks, step-up CAPTCHA, MFA) and justify the ordering.

πŸ“„ View solution

Chapter 3 Quick Reference

  • Brute force / dictionary β€” many guesses vs one account; credential stuffing β€” replays real breached pairs; spraying β€” few common passwords vs many accounts
  • Credential stuffing is the dominant real-world ATO attack β€” known-correct credentials, exploiting password reuse
  • Rate limit on multiple keys (IP and account) β€” per-account misses spraying, per-IP misses botnets
  • Account lockout is double-edged β€” naive hard locks enable DoS against your own users & miss spraying; prefer backoff + temporary locks + step-up
  • Prevent user enumeration β€” identical generic responses on login/signup/reset, and equalize timing (dummy hash when user absent)
  • CAPTCHA / risk-based step-up after signs of abuse, not on every login
  • Breach checks (HIBP k-anonymity) + length over complexity + no forced rotation (NIST guidance)
  • MFA outranks all of these β€” it makes a guessed/stuffed password insufficient (Chapter 6)
  • Next chapter: sessions & cookies β€” how the session works after login
Chapter 4 of 10

Sessions & Cookies

Chapter 4
Sessions & Cookies
How the session works after login β€” session IDs, cookies, server-side vs client-side

Authentication (Chapters 2–3) happens once, at login. But HTTP is stateless β€” every request is independent, with no memory of the last. So how does the server know that this request comes from someone who logged in five minutes ago? The answer is the session: the mechanism that carries "you are logged in" across many stateless requests. This is the thing all three previous courses kept calling "the session."

The Problem: HTTP Has No Memory

Each HTTP request stands alone. Without a session, a user would have to send their password on every single request β€” terrible for security (the credential constantly in flight) and UX. The session solves this: log in once, then prove "I'm still that logged-in user" cheaply on each subsequent request, without resending the password.

The Session ID: A Bearer Token for "Logged In"

After a successful login, the server creates a session and issues a session ID β€” a long, random, unguessable string. The client sends that ID back on every request, and the server uses it to recognize the user. The login lifecycle:

User submits correct credentials (Chapters 2–3 verify them).
Server creates a session and generates a random session ID (high entropy, unguessable).
Server sends the ID to the client, normally in a cookie (Set-Cookie: sessionId=…).
Browser automatically returns the cookie on every subsequent request to that site.
Server looks up the session by ID β†’ knows who the user is, no password needed.
The session ID IS the identity β€” anyone holding it is "you"
A session ID is a bearer token: whoever presents it is treated as the authenticated user, no questions asked. The server doesn't re-check your password β€” it trusts the ID. That's exactly why stealing the session ID (XSS reading document.cookie, or sniffing it over plain HTTP) equals account takeover without the password (XSS course Ch. 5), and why CSRF can ride it (CSRF course). The entire rest of this course's session material is about protecting this one secret string β€” keeping it confidential, unguessable, and short-lived. Treat the session ID with the same care as a password.

Cookies: The Usual Session Carrier

The session ID needs somewhere to live in the browser and ride along on requests. The standard home is a cookie, because cookies are sent automatically by the browser to their origin β€” the user (and the app code) doesn't have to attach the ID manually:

# server sets the session cookie on login Set-Cookie: sessionId=9f8c1a...e2; HttpOnly; Secure; SameSite=Lax; Path=/ # browser auto-sends it on every later request to the site Cookie: sessionId=9f8c1a...e2

That "sent automatically" property is convenient β€” and is precisely the double-edged sword behind CSRF (the cookie rides even on forged cross-site requests). The cookie's security attributes β€” HttpOnly, Secure, SameSite β€” are what make it safe to carry a session ID; they get their own deep treatment in the next chapter. (Session IDs can also be carried in other ways β€” e.g. an Authorization header for APIs β€” covered with tokens in Chapter 7.)

Where the Session Data Lives: Server-Side vs Client-Side

There are two fundamentally different models for where the actual session state is stored. This is one of the most important architectural choices in auth:

Server-side (stateful)Client-side (stateless)
Cookie holdsjust an opaque session IDthe session data itself (signed/encrypted)
Data stored inserver memory / Redis / databasethe cookie (or a token) on the client
Server lookupyes β€” find session by ID each requestno β€” verify the signature & read it
Revocationeasy β€” delete the server recordhard β€” can't un-issue a self-contained token
Scalingneeds shared store across serversno shared store needed

Server-Side Sessions (the classic, default model)

The cookie holds only a meaningless random ID; all the real data (user ID, roles, login time) lives on the server, keyed by that ID. This is the traditional approach (PHP sessions, Express express-session, Rails) and the safest default. Its standout advantage: instant revocation β€” to log someone out everywhere (or kill a stolen session), you just delete the server-side record and the ID becomes worthless.

Client-Side Sessions (stateless)

The cookie (or token) contains the session data itself, protected by a cryptographic signature so the client can't tamper with it. The server stores nothing β€” it just verifies the signature and reads the data. This scales beautifully (no shared session store) and is the model behind JWTs (Chapter 7).

The core trade-off: easy scaling vs easy revocation
Stateless (client-side) sessions are wonderful for scaling β€” any server can validate the token with just the signing key, no database hit, no shared session store. But they have a hard problem: revocation. Because the token is self-contained and valid until it expires, you can't simply "delete" it β€” if it's stolen, it keeps working until expiry. Server-side sessions are the mirror image: a quick lookup per request and a shared store to manage, but you can revoke instantly by deleting the record. There's no free lunch β€” Chapter 7 covers the patterns (short-lived tokens + refresh, denylists) used to claw back revocation for stateless sessions. For most apps, server-side sessions remain the simpler, safer default.

What Makes a Good Session ID

  • High entropy β€” long and generated by a cryptographically secure RNG (β‰₯128 bits), so it can't be guessed or brute-forced.
  • Opaque β€” meaningless and random; it should encode nothing (not the user ID, not a counter). A predictable or sequential ID is a catastrophe (Exercise 3).
  • Unique β€” never reused across sessions or users.
  • Generated by the framework β€” use your framework's session machinery (it uses a CSPRNG); never hand-roll IDs from timestamps, counters, or Math.random().

Hands-On Exercises

Exercise 1

Explain why HTTP being stateless makes sessions necessary, and trace the five-step session lifecycle from login to a later authenticated request. Identify precisely why the server doesn't need the password again after step 1, and what the session ID represents.

πŸ“„ View solution
Exercise 2

Compare server-side and client-side sessions across storage, per-request cost, scaling, and revocation. For each of these requirements, recommend a model and justify: (a) "log out this stolen session immediately"; (b) "scale to 50 stateless API servers with no shared store."

πŸ“„ View solution
Exercise 3

A site issues sequential session IDs (user-1001, user-1002, …) or IDs derived from Math.random()/timestamps. Explain the attack each enables and how an attacker hijacks other users' sessions. State the properties a session ID must have and why the framework's generator should be used.

πŸ“„ View solution

Chapter 4 Quick Reference

  • HTTP is stateless β†’ the session carries "you are logged in" across requests without resending the password
  • Session ID = a long random unguessable string issued at login; the client returns it each request
  • The session ID is a bearer token β€” whoever holds it is "you"; protect it like a password (XSS theft / CSRF riding target it)
  • Cookies are the usual carrier (auto-sent to origin); their HttpOnly/Secure/SameSite attributes secure it (next chapter)
  • Server-side (stateful) β€” cookie holds only an opaque ID; data on server; easy revocation, needs shared store
  • Client-side (stateless) β€” cookie/token holds signed data; scales without a store; hard to revoke (JWTs, Ch. 7)
  • Core trade-off: scaling (stateless) vs revocation (stateful); server-side is the simpler, safer default for most apps
  • Good session ID: high-entropy (β‰₯128-bit CSPRNG), opaque, unique, framework-generated β€” never sequential/predictable
  • Next chapter: session security β€” fixation, hijacking, rotation, timeouts, and the cookie flags in full
Chapter 5 of 10

Session Security

Chapter 5
Session Security
Fixation, hijacking, rotation, timeouts β€” and the cookie flags in full

The session ID is a bearer token (Chapter 4) β€” hold it and you're the user. This chapter is about protecting that token through its whole life: stopping it being planted (fixation), stolen (hijacking), or kept alive too long (timeouts), and configuring the cookie that carries it. This is where the HttpOnly / Secure / SameSite advice from the HTTPS, CSRF, and XSS courses finally comes home β€” on the session cookie.

Session Hijacking: Stealing the Token

Session hijacking means obtaining a valid session ID and using it to impersonate the user. The token can be stolen several ways β€” each of which a previous course addressed:

Theft routeDefenceCourse
Sniffing plaintext HTTPHTTPS everywhere + HSTS + Secure cookieHTTPS
XSS reading the cookieHttpOnly cookie + fix XSSXSS
Riding the session cross-siteSameSite + CSRF tokensCSRF
Guessing a predictable IDhigh-entropy CSPRNG IDsAuth Ch. 4

Session Fixation: Planting a Known Token

A more subtle attack. Instead of stealing your session ID, the attacker gives you one they already know, then waits for you to log in with it:

// Session fixation, step by step: 1. Attacker obtains a valid pre-login session ID (e.g. visits the site). 2. Attacker tricks the victim into using THAT id (a crafted link, or by setting the cookie via another vuln). 3. Victim logs in β€” and the server keeps the same session id. 4. The id is now an AUTHENTICATED session β€” and the attacker knows it. The attacker uses it and is logged in as the victim.
The fix is one rule: regenerate the session ID on every privilege change
Session fixation works only if the session ID stays the same across the login boundary. The defence is simple and non-negotiable: generate a brand-new session ID at login (and at any privilege elevation β€” e.g. stepping up to admin). The pre-login ID the attacker planted becomes worthless because the authenticated session has a fresh, unknown ID. Frameworks expose this as session.regenerate() / equivalent β€” and many do it automatically on login, but verify yours does. (This is also, you may recall, the most likely cause of the "Invalid CSRF token" login error from the CSRF course: the CSRF token was tied to the pre-regeneration session.)

Session Rotation More Broadly

Regenerating the ID isn't only for login. Good practice rotates the session ID at key moments to limit the value of any single captured token:

  • At login β€” defeats fixation (above); the most important one.
  • At privilege elevation β€” e.g. entering an admin area or re-authenticating for a sensitive action.
  • Periodically β€” some apps rotate the ID every N requests/minutes, so a stolen ID has a short useful life (balance against UX/complexity).
  • On logout β€” fully destroy the server-side session, don't just clear the cookie (next section).

Timeouts: Idle vs Absolute

A session shouldn't live forever β€” a stolen or abandoned session is a standing risk. Two complementary timeouts:

TimeoutExpires after…Purpose
Idle (inactivity)N minutes of no activitykills abandoned sessions (e.g. walked away from a shared PC)
Absolutea fixed max lifetime since login (e.g. 8–24h)caps how long any session β€” even an active stolen one β€” can live

Use both: idle timeout for the common "forgot to log out" case, and an absolute cap so an attacker who steals a session can't keep renewing it indefinitely by staying active. Sensitive apps (banking) use short timeouts; consumer apps balance security against the annoyance of frequent re-login. Pair timeouts with proper logout: destroy the server-side session record and clear the cookie β€” clearing only the cookie leaves a valid session on the server that a stolen ID could still use.

The Cookie Flags β€” In Full, at Last

Everything in this course's quartet converges on these attributes of the session cookie. Set all of them:

Set-Cookie: sessionId=9f8c…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
FlagEffectStops
HttpOnlycookie unreadable by JavaScriptXSS stealing the session via document.cookie
Securecookie sent only over HTTPSsniffing the cookie on plaintext connections
SameSite=Lax/Strictnot sent on (most) cross-site requestsCSRF riding the session
__Host- prefixforces Secure, Path=/, no Domaincookie injection from subdomains
The whole quartet in four attributes
Look at what these flags map to: Secure is the HTTPS course (don't leak it on the wire), HttpOnly is the XSS course (script can't read it), SameSite is the CSRF course (don't send it cross-site), and the cookie carries the session this course is about. Four words on one Set-Cookie header encode the lessons of all four courses. Two honest caveats carried from those courses: HttpOnly stops cookie theft but not session riding (XSS can still act in-session β€” fix XSS), and SameSite is strong but not complete (state-changing GETs, same-site subdomains β€” keep CSRF tokens). Defence in depth, as always.

Hands-On Exercises

Exercise 1

Explain session fixation step by step and why regenerating the session ID at login defeats it. Then explain the connection to the "Invalid CSRF token" login error from the CSRF course, and list the other moments a session ID should be rotated.

πŸ“„ View solution
Exercise 2

For the session cookie, write the complete Set-Cookie with all security flags, and for each flag state which theft/abuse route it blocks and which prior course it comes from. Then explain why HttpOnly alone doesn't make XSS harmless, and why SameSite alone doesn't fully replace CSRF tokens.

πŸ“„ View solution
Exercise 3

Distinguish idle vs absolute timeout and explain why you want both. Then explain why "logout" must destroy the server-side session rather than only clearing the cookie, and what goes wrong if you only clear the cookie while a stolen ID exists.

πŸ“„ View solution

Chapter 5 Quick Reference

  • Session hijacking = stealing a valid ID (sniffing β†’ Secure/HTTPS; XSS β†’ HttpOnly; cross-site β†’ SameSite; guessing β†’ entropy)
  • Session fixation = attacker plants a known pre-login ID; defeated by regenerating the ID at login (and privilege elevation)
  • Fixation regeneration is also the likely cause of the CSRF course's "Invalid CSRF token" login error (token tied to the old session)
  • Rotate the ID: at login, at privilege elevation, optionally periodically, and destroy it on logout
  • Idle timeout (kills abandoned sessions) + absolute timeout (caps total lifetime) β€” use both
  • Logout = destroy the server-side session record and clear the cookie β€” not just the cookie
  • Cookie flags: HttpOnly (XSS), Secure (HTTPS), SameSite (CSRF), __Host- prefix β€” the whole quartet in one header
  • Caveats: HttpOnly stops theft not riding; SameSite β‰  full CSRF defence β€” keep defence in depth
  • Next chapter: multi-factor authentication β€” TOTP, passkeys/WebAuthn, and why SMS is weak
Chapter 6 of 10

Multi-Factor Authentication

Chapter 6
Multi-Factor Authentication
TOTP, WebAuthn / passkeys, and why SMS is the weakest factor

Chapter 3 ended on a promise: passwords alone are guessable, stuffable, and phishable, but MFA changes the game β€” a correct password is no longer enough. This chapter covers the second-factor options, from the common (TOTP apps) to the gold standard (passkeys/WebAuthn), and is honest about which are strong and which only look strong (SMS).

Why MFA Works

Recall the factor categories (Chapter 1): knowledge (know), possession (have), inherence (are). MFA requires factors from different categories, so an attacker must compromise independent channels. Your phished or stuffed password (knowledge) is useless without also defeating your possession/inherence factor. That independence is the entire value β€” it's why MFA is the single highest-leverage account-security control.

The Second-Factor Options

MethodFactorPhishing-resistant?Strength
SMS / email OTPpossession (weak)NoWeakest β€” avoid where possible
TOTP (authenticator app)possessionNo (code can be phished)Good β€” widely available
Push approvalpossessionNo (MFA fatigue)OK β€” number-matching helps
Hardware OTP tokenpossessionNoGood
WebAuthn / passkeypossession (+inherence)Yes β€” origin-boundStrongest

TOTP β€” Time-Based One-Time Passwords

The familiar 6-digit authenticator-app code (Google Authenticator, Authy, etc.). At setup, the server and the app share a secret (shown as a QR code). Both then compute the same code from HMAC(secret, current_30-second_time_window) β€” so the code changes every 30 seconds and the app needs no network connection:

// both sides derive the same 6-digit code, no network needed code = truncate( HMAC_SHA1(sharedSecret, floor(unixTime / 30)) ) % 1000000

TOTP is a solid, ubiquitous improvement over passwords alone. Its limits: the shared secret exists on the server (a breach could expose it), and β€” crucially β€” the code can be phished. A fake login page can ask for your password and your current TOTP code and relay both to the real site in real time. TOTP raises the bar a lot, but a determined phisher can defeat it.

The SMS Problem

SMS OTP is the weakest second factor β€” better than nothing, worse than everything else
Texting a code to a phone feels like "something you have," but the phone number isn't securely bound to you. SIM-swapping β€” an attacker social-engineers your carrier into porting your number to their SIM β€” redirects your codes to them; it's a common, well-documented account-takeover technique. SMS is also vulnerable to SS7 network interception, malware, and lock-screen previews. NIST has discouraged SMS as a second factor for years. Use it only as a last-resort option for users who can't do anything else β€” and never as the only or recovery factor for high-value accounts. Prefer TOTP at minimum; passkeys ideally.

Push Notifications & MFA Fatigue

"Approve this login?" push prompts are convenient, but they introduced a new attack: MFA fatigue (push bombing). An attacker with your password triggers login repeatedly, spamming your phone with approval prompts until you tap "Approve" out of annoyance or confusion (this was the vector in several high-profile 2022 breaches). The mitigation is number matching β€” the screen shows a number you must type into the app, so you can't approve blindly β€” plus rate-limiting prompts and showing context (location, app).

WebAuthn / Passkeys β€” The Gold Standard

WebAuthn (the web standard behind passkeys) is fundamentally stronger because it's built on public-key cryptography and is phishing-resistant by design. At registration your device (phone, laptop, or a hardware key like a YubiKey) generates a key pair: the private key never leaves the device, and the site stores only the public key. At login, the site sends a challenge, the device signs it with the private key (after a local biometric/PIN unlock), and the site verifies the signature.

Why passkeys can't be phished: the credential is bound to the origin
The breakthrough property: a WebAuthn credential is cryptographically bound to the website's origin. Your device will only sign a challenge for the exact origin the passkey was registered to. So if you're lured to g00gle-login.com, your google.com passkey simply won't respond β€” there's no code to read out and type into the fake site, and nothing to relay. This defeats the real-time phishing proxy that beats TOTP. Combined with the private key never leaving the device and a biometric unlock (possession + inherence), passkeys eliminate entire attack classes: phishing, credential stuffing, password reuse, and server-side secret theft (the server holds only a public key, useless to a thief). This is why the industry is moving toward passkeys as a password replacement, not just a second factor.

Recovery Codes β€” The Backup You Must Plan For

MFA introduces a new failure mode: what if the user loses the factor (lost phone, dead hardware key)? Without a recovery path they're locked out; with a weak one, you've added a back door. The standard answer is one-time recovery codes β€” a set of single-use backup codes generated at MFA setup, shown once, that the user stores safely.

Recovery is where MFA is most often undermined β€” secure it like a factor
Strong MFA with a weak recovery flow is no stronger than its recovery flow. If "lost your authenticator?" falls back to an SMS code or an email link, you've quietly reduced your strong MFA to the security of SMS/email (and the email account may itself be the target β€” Chapter 9). Treat recovery codes as secrets: store them hashed (like passwords), invalidate each after use, and require the same rigor (rate limiting, no enumeration) on the recovery path as on login. Account recovery is the #1 takeover vector β€” Chapter 9 is dedicated to getting it right.

Hands-On Exercises

Exercise 1

Explain why MFA defeats credential stuffing and password phishing that single-factor login cannot, in terms of "independent channels." Then explain why TOTP, despite being a real second factor, can still be defeated by a real-time phishing proxy.

πŸ“„ View solution
Exercise 2

Explain why SMS is considered the weakest second factor (name at least two concrete attacks), and why "push approval" introduced MFA fatigue. Give the mitigation for push bombing and state where SMS is still acceptable, if anywhere.

πŸ“„ View solution
Exercise 3

Explain why WebAuthn/passkeys are phishing-resistant where TOTP is not, focusing on origin binding and the public/private key split. Then explain why a strong MFA setup with an SMS-based recovery fallback is only as strong as the SMS, and how recovery codes should be stored.

πŸ“„ View solution

Chapter 6 Quick Reference

  • MFA requires factors from different categories β†’ attacker must beat independent channels; a stuffed/phished password alone fails
  • TOTP β€” 6-digit app code from HMAC(sharedSecret, time/30); good & ubiquitous, but the code is phishable (real-time proxy)
  • SMS OTP β€” weakest: SIM-swap, SS7 interception, previews; NIST-discouraged; last resort only
  • Push approval β€” convenient but enables MFA fatigue / push bombing; mitigate with number matching + rate limits + context
  • WebAuthn / passkeys β€” public-key, private key never leaves device, biometric unlock; phishing-resistant (origin-bound) β€” the gold standard
  • Passkeys can't be phished because the credential is bound to the origin β€” won't respond to a lookalike site; server stores only a public key
  • Recovery codes β€” single-use backups, shown once, stored hashed; don't fall back to SMS/email and undo your strong MFA
  • Recovery is the most-undermined part β€” secure it as rigorously as login (Chapter 9)
  • Next chapter: token-based auth & JWTs β€” access/refresh tokens, signing pitfalls, storage, revocation
Chapter 7 of 10

Token-Based Auth & JWTs

Chapter 7
Token-Based Auth & JWTs
Access & refresh tokens, JWT signing pitfalls, storage, and revocation

Chapter 4 introduced the stateless (client-side) session and promised to return to it. Here it is. Token-based authentication β€” most commonly with JWTs β€” replaces the server-side session lookup with a self-contained, signed token the client carries. It's the backbone of APIs and SPAs, and it's powerful, but it has sharp edges: signing pitfalls, storage dilemmas, and the revocation problem from Chapter 4.

What a JWT Is

A JSON Web Token is three Base64URL parts joined by dots: header.payload.signature. The header names the signing algorithm, the payload holds claims (who the user is, when it expires), and the signature proves the token was issued by the server and not tampered with:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3...}.SflKxwRJSMeKKF2QT4... └── header β”€β”€β”˜ └──── payload β”€β”€β”€β”€β”˜ └── signature β”€β”€β”˜ // decoded header + payload (claims) header = { "alg": "HS256", "typ": "JWT" } payload = { "sub": "123", "role": "user", "exp": 1730000000 }
A JWT is signed, NOT encrypted β€” the payload is readable by anyone
The header and payload are merely Base64-encoded, not encrypted β€” anyone holding the token can decode and read the claims (paste it into jwt.io and see). The signature only guarantees integrity (it wasn't altered) and authenticity (the server issued it) β€” it does not hide anything. So never put secrets in a JWT payload (passwords, card numbers, anything sensitive). What stops tampering is that changing a claim invalidates the signature, which the attacker can't recompute without the signing key β€” but they can always read it.

Access Tokens & Refresh Tokens

The standard pattern splits responsibilities across two tokens to balance the stateless-vs-revocation trade-off (Chapter 4):

Access tokenRefresh token
Purposeauthorize each API requestobtain a new access token when it expires
Lifetimeshort (minutes)long (days/weeks)
Sentwith every requestonly to the token-refresh endpoint
If stolenlimited window (expires soon)serious β€” but can be revoked server-side

The logic: the access token is short-lived so a stolen one is only useful briefly (mitigating the revocation problem), while the refresh token is long-lived but tightly guarded and revocable β€” typically tracked server-side so it can be invalidated. This gives you most of stateless scaling with a practical revocation path.

JWT Signing Pitfalls

JWTs have a notorious history of implementation flaws. The classics:

The alg: none and algorithm-confusion attacks
(1) alg: none β€” the JWT spec includes a "none" algorithm meaning "unsigned." A naive verifier that trusts the token's own alg header can be handed a token with {"alg":"none"} and no signature β€” and accept it. The attacker forges any claims they like (e.g. "role":"admin"). Fix: the server must enforce the expected algorithm, never trust the token's header to choose it. (2) Algorithm confusion (RS256 β†’ HS256) β€” if the server uses RSA (public/private) but a flawed verifier lets an attacker switch alg to HS256 (symmetric), the attacker can sign a forged token using the public key (which is, well, public) as the HMAC secret. Fix: pin the exact algorithm and key type. (3) Weak HMAC secrets β€” a short/guessable HS256 secret can be brute-forced offline, letting the attacker mint valid tokens. Use a long, random secret. The umbrella rule: use a vetted JWT library, configure the exact algorithm explicitly, and never let the token decide how it's verified.

The Revocation Problem β€” and How Tokens Address It

A pure JWT is valid until it expires; you can't "delete" a self-contained token (Chapter 4). For logout, stolen-token response, or "ban this user now," you need a strategy:

  • Short access-token lifetimes β€” the simplest mitigation; a stolen access token dies in minutes. The window, not the token, is what you control.
  • Revocable refresh tokens β€” store refresh tokens server-side (a small amount of state) so you can revoke them; on logout/compromise, delete them and the user can't get new access tokens.
  • A denylist / blocklist β€” keep a list of revoked token IDs (the jti claim) checked on each request β€” but this reintroduces server-side state and a lookup, partly defeating statelessness.
  • Token versioning β€” store a per-user token version; bump it to invalidate all that user's tokens at once (e.g. on password change).
Refresh-token rotation & reuse detection β€” the modern best practice
A strong pattern: rotate the refresh token on every use (each refresh issues a new refresh token and invalidates the old one), and detect reuse. If an old, already-rotated refresh token is ever presented again, that's a strong signal it was stolen and replayed β€” so the server revokes the entire token family, forcing re-authentication. This catches refresh-token theft that short access-token lifetimes alone would miss, and is what well-built JWT systems (and libraries like those following the OAuth BCP) implement.

Where to Store Tokens (the Cookie vs localStorage Debate)

This connects straight back to the CSRF and XSS courses β€” there's a genuine trade-off, not a single right answer:

StorageCSRF riskXSS riskNotes
HttpOnly cookieyes β€” needs CSRF defencestoken not readable by scriptauto-sent; safest vs XSS theft
localStorage / JS memoryno β€” not auto-sentreadable by any XSSmust attach via Authorization header

Storing the token in an HttpOnly cookie protects it from XSS theft but reintroduces CSRF (the cookie auto-sends) β€” so you add SameSite + CSRF tokens. Storing it in localStorage and sending it in an Authorization: Bearer header avoids classic CSRF but exposes the token to any XSS. As the CSRF course concluded: there's no "JWT = safe" β€” the risk tracks where you store it. For browser apps, an HttpOnly cookie (plus CSRF defences) is often the safer default, because XSS theft of a bearer token is full account takeover with no expiry help.

JWT vs Server-Side Sessions β€” When to Use Which

Don't reach for JWTs reflexively. For a single monolithic web app, classic server-side sessions (Chapter 4) are simpler, instantly revocable, and perfectly scalable with a shared store β€” often the better choice. JWTs shine for stateless APIs, microservices, and cross-service auth where avoiding a shared session lookup genuinely matters. Choose by your architecture, not by fashion.

Hands-On Exercises

Exercise 1

Decode the three parts of a JWT and explain what each does. Then explain why you must never store sensitive data in the payload, and what the signature does and doesn't protect (integrity/authenticity vs confidentiality).

πŸ“„ View solution
Exercise 2

Explain the alg: none and RS256β†’HS256 algorithm-confusion attacks, including how each forges a valid-looking token. Give the fix for both, and explain why a verifier must never trust the token's own alg header.

πŸ“„ View solution
Exercise 3

Explain the revocation problem for stateless JWTs and how the access/refresh split plus short lifetimes mitigate it. Then describe refresh-token rotation with reuse detection and what it catches. Finally, recommend a token-storage approach for a browser SPA and justify it against CSRF and XSS.

πŸ“„ View solution

Chapter 7 Quick Reference

  • JWT = header.payload.signature (Base64URL); self-contained, signed, stateless
  • Signed, not encrypted β€” payload is readable by anyone; signature gives integrity + authenticity, not confidentiality; never store secrets in it
  • Access token (short-lived, every request) + refresh token (long-lived, guarded, revocable) β€” balances scaling vs revocation
  • Signing pitfalls: alg:none (unsigned accepted), RS256β†’HS256 confusion (sign with the public key), weak HMAC secret β€” pin the exact algorithm, never trust the token's alg
  • Revocation problem: can't delete a self-contained token β€” use short TTLs, revocable refresh tokens, denylist (jti), or token versioning
  • Refresh rotation + reuse detection β€” new refresh token each use; replay of an old one β†’ revoke the whole family (catches theft)
  • Storage: HttpOnly cookie (XSS-safe, needs CSRF defences) vs localStorage (CSRF-free, XSS-exposed) β€” risk tracks storage, no "JWT = safe"
  • JWT vs server-side sessions: sessions simpler & instantly revocable for monoliths; JWTs for stateless APIs/microservices β€” choose by architecture
  • Next chapter: OAuth 2.0 & OpenID Connect β€” delegated authorization, social login, and the flows
Chapter 8 of 10

OAuth 2.0 & OpenID Connect

Chapter 8
OAuth 2.0 & OpenID Connect
Delegated authorization vs authentication, social login, and the flows

"Sign in with Google." "Allow this app to access your calendar." These are OAuth 2.0 and OpenID Connect at work β€” the protocols for letting one app act on your behalf at another without giving it your password. They're widely used and widely misunderstood, starting with the most important distinction: OAuth is about authorization, OIDC adds authentication.

The Problem OAuth Solves: Delegated Access Without Sharing Passwords

Suppose a photo-printing app wants to access your Google Photos. The naive (terrible) way: you give the app your Google password. Now it can do anything as you, forever, and you can't revoke it without changing your password. OAuth 2.0 solves this: it lets you grant the app limited, revocable access to specific resources, without ever sharing your credentials. The app gets an access token scoped to just what you approved.

The Four Roles

RoleWhoExample
Resource Owneryou (the user)you, who owns the photos
Clientthe app wanting accessthe photo-printing app
Authorization Serverissues tokens after you consentGoogle's OAuth server
Resource Serverholds the protected datathe Google Photos API

OAuth Is Authorization β€” OIDC Adds Authentication

The #1 OAuth misconception: OAuth alone does NOT authenticate the user
OAuth 2.0 was designed for delegated authorization β€” "this app may access these resources" β€” not for proving who the user is. People bolted "login with OAuth" on top anyway, which led to real vulnerabilities, because an access token says what the bearer may do, not who they are. OpenID Connect (OIDC) is the thin layer added on top of OAuth 2.0 to do authentication properly: it adds an ID token (a JWT, Chapter 7) containing verified identity claims (who the user is, issued by the provider, for your app). Rule of thumb: OAuth = authorization (access), OIDC = authentication (identity). If you're doing "Sign in with…", you want OIDC, not raw OAuth.

The Authorization Code Flow (with PKCE)

The recommended flow for web and mobile apps. The key idea: the app never sees your password, and the powerful token is exchanged through a back channel, not exposed in the browser. Step by step:

You click "Sign in with Google." The app redirects you to Google's authorization server (with its client ID, requested scopes, and a state value).
You authenticate directly with Google (password + MFA) and approve the requested scopes. The app never sees your credentials.
Google redirects you back to the app with a short-lived authorization code (not a token).
The app's server exchanges that code (plus its client secret) with Google's token endpoint β€” a back-channel request β€” for the access token (and an ID token, if OIDC).
The app uses the access token to call the API on your behalf, limited to the approved scopes.
Why a code first, and why PKCE β€” defeating interception
Why not hand the app the token directly? Because the redirect goes through the browser, where URLs can leak (history, referrer, logs). So the browser only ever carries a short-lived, single-use authorization code; the actual token is fetched server-to-server. PKCE (Proof Key for Code Exchange) hardens this further for public clients (SPAs, mobile apps that can't keep a secret): the app generates a random code_verifier, sends its hash (code_challenge) when starting the flow, and must present the original verifier when exchanging the code. So even if an attacker intercepts the authorization code, they can't exchange it without the verifier β€” which never left the legitimate app. PKCE is now recommended for all clients, not just public ones.

Scopes: Least Privilege for Delegated Access

A scope is a named permission the client requests (photos.read, calendar.write, email). The consent screen shows you exactly what you're granting, and the token is limited to those scopes. This is least privilege applied to delegation: request only the scopes you actually need, and users (rightly) distrust apps asking for more than they should. Over-broad scope requests are both a security risk and a conversion killer.

Common Misconfigurations

Where OAuth/OIDC integrations go wrong
(1) Missing state β†’ CSRF on the callback. The state parameter is a random value the app sends and verifies on return; without it, an attacker can splice their own authorization code into your session (a login-CSRF). Always generate, store, and check state. (2) Loose redirect_uri validation. If the authorization server allows wildcard or unregistered redirect URIs, an attacker can redirect the code/token to a site they control. Register exact redirect URIs. (3) Using the implicit flow. The old "implicit" flow returned tokens directly in the browser URL fragment β€” exposed and unrevocable; it's deprecated. Use authorization code + PKCE. (4) Treating an OAuth access token as proof of identity. An access token isn't an ID token β€” verify the OIDC ID token's signature, issuer (iss), audience (aud), and expiry before trusting its identity claims. (5) Not verifying the ID token at all β€” accepting it without checking the signature/issuer lets an attacker forge identities.

Social Login: What You Gain and Give Up

"Sign in with Google/Apple/GitHub" (OIDC) means users don't create yet another password, you don't store credentials, and you inherit the provider's strong authentication (their MFA, their anti-abuse). The trade-offs: you depend on the provider (outages, account bans, policy changes cascade to your users), you share some user data with them, and you must still handle account linking (same person, multiple providers) and the case where a user loses access to their provider account. For many apps it's a net win; weigh it deliberately rather than adding every provider button reflexively.

Hands-On Exercises

Exercise 1

Explain the difference between OAuth 2.0 and OpenID Connect in terms of authorization vs authentication, and why using a raw OAuth access token as "proof of who the user is" is a security mistake. State what OIDC adds and which token carries identity.

πŸ“„ View solution
Exercise 2

Walk through the authorization code flow and explain why the browser only carries a short-lived code (not the token), and why the token is exchanged server-to-server. Then explain what PKCE adds and how it defeats authorization-code interception.

πŸ“„ View solution
Exercise 3

Identify the security problem in each: (a) the app omits the state parameter; (b) the authorization server accepts any redirect_uri under the domain; (c) the app uses the implicit flow; (d) the app trusts the ID token's claims without verifying its signature/issuer/audience. Give the fix for each.

πŸ“„ View solution

Chapter 8 Quick Reference

  • OAuth 2.0 = delegated authorization β€” grant an app limited, revocable access to resources without sharing your password
  • Four roles: Resource Owner (you), Client (app), Authorization Server (issues tokens), Resource Server (holds data)
  • OAuth = authorization; OIDC = authentication β€” OIDC adds an ID token (JWT) with verified identity claims; use OIDC for "Sign in with…"
  • Authorization Code flow: browser carries a short-lived code; the token is exchanged server-to-server (back channel)
  • PKCE β€” verifier/challenge so an intercepted code can't be exchanged; recommended for all clients
  • Scopes = least-privilege permissions shown on the consent screen; request only what you need
  • Misconfigs: missing state (callback CSRF) Β· loose redirect_uri Β· implicit flow (deprecated) Β· trusting an access token as identity Β· unverified ID token
  • Social login: no passwords to store + inherit provider's MFA, but you depend on the provider & share data β€” weigh deliberately
  • Next chapter: account recovery & verification β€” secure password reset, email verification, the #1 takeover vector
Chapter 9 of 10

Account Recovery & Verification

Chapter 9
Account Recovery & Verification
Secure password reset, email verification β€” the #1 takeover vector

Every chapter so far has secured the front door. This chapter secures the side door β€” account recovery β€” and it's the one attackers love most, because a recovery flow is a deliberate way to gain access without the original credential. Get it wrong and all your strong passwords, MFA, and session hardening are bypassed. This is the chapter the CSRF course's "change email β†’ password reset β†’ takeover" chain has been pointing at all along.

Account recovery is, by design, an authentication bypass β€” so it must be as strong as login
A password-reset flow exists precisely to let someone in who doesn't have the password. That makes it an alternate authentication path β€” and attackers attack the weakest path, not the strongest (Chapter 6). If your login uses passkeys and MFA but your reset flow emails a link, an attacker simply ignores the front door and attacks the email-based reset. Recovery is consistently the #1 account-takeover vector. Treat it with the same (or greater) rigor as login: it deserves rate limiting, anti-enumeration, secure tokens, and re-verification β€” not a quick "email them a link."

The Secure Password-Reset Flow

The correct shape uses a secure, single-use, expiring token delivered out-of-band (email), never a security question or a guessable code:

User requests a reset for an email. Respond with a generic message regardless of whether the account exists (anti-enumeration).
If the account exists, generate a high-entropy random token (CSPRNG), store only its hash with a short expiry and the user ID.
Email a link containing the token: /reset?token=… β€” sent only to the registered address.
User clicks; the server hashes the presented token and looks it up β€” valid, unexpired, unused?
User sets a new password; the server invalidates the token (single-use), invalidates existing sessions, and notifies the user.

Reset-Token Design β€” The Details That Matter

PropertyWhy
High entropy (CSPRNG)unguessable / unbrute-forceable β€” it's a temporary credential
Stored hasheda DB leak mustn't reveal usable reset tokens (treat like a password)
Short expirylimits the window a leaked link is useful (e.g. 15–60 min)
Single-useinvalidate on use (and on a new request) so it can't be replayed
Bound to one accounttoken maps to a specific user; can't be redirected
Three classic reset-flow bugs
(1) Predictable tokens β€” sequential IDs, timestamps, or Math.random() let an attacker forge a valid reset link for any account (same lesson as session IDs, Chapter 4). Use a CSPRNG. (2) Tokens that don't expire / aren't single-use β€” a reset link that works forever is a permanent backdoor sitting in an inbox; one that's reusable can be replayed. Expire and invalidate. (3) Host-header / link poisoning β€” if the reset link's domain is built from the request's Host header, an attacker can request a reset for a victim, spoof the Host so the emailed link points at the attacker's site, and capture the token when the victim clicks. Build reset URLs from a trusted, configured base URL β€” never from the incoming request's host.

Don't Use Security Questions

"What's your mother's maiden name?" / "First pet?" are knowledge factors (Chapter 1) that are often public or guessable β€” found on social media, in public records, or simply common (a handful of pet names cover most answers). They're a weak recovery method that has enabled many real takeovers. Avoid them; if a legacy system has them, treat them as one weak signal among several, never as the sole gate.

Email Verification

Verifying a new user actually controls the email they signed up with uses the same token mechanism, and matters for several reasons: it confirms the address is reachable (so recovery works), prevents signups under someone else's email, and reduces spam/abuse accounts. The flow mirrors reset: generate a high-entropy token, store it hashed with an expiry, email a single-use verification link, and mark the address verified when clicked. Keep responses generic (don't reveal "this email is already registered" to a stranger β€” that's enumeration, Chapter 3).

Re-Verification & Notifications for Sensitive Changes

Recovery security extends to changing the recovery channels themselves β€” the exact step the CSRF takeover chain exploited:

  • Re-authenticate for sensitive changes β€” require the current password (or an MFA challenge) before changing email, password, or MFA settings. A forged or session-riding request then can't succeed (CSRF/XSS courses).
  • Notify on every security change β€” email the user (at the old address too, for an email change) when the password, email, or MFA is changed, so an unauthorized change is at least visible and reversible.
  • Invalidate sessions on password reset β€” after a reset, kill all existing sessions, so an attacker who had a session is logged out by the legitimate recovery.
  • Cool-down / delayed change for email β€” some systems delay or allow a grace-period reversal of an email change, blunting silent takeover.
This closes the CSRF course's account-takeover chain
Recall the chain: a forged change-email request β†’ attacker controls the recovery address β†’ triggers a password reset β†’ seizes the account. This chapter is the defence, end to end: re-authentication on the change-email step stops the forged request from succeeding; notification to the old address surfaces it if it somehow does; generic responses + rate limiting stop the attacker probing accounts; and session invalidation on reset ejects them. The recovery flow isn't an afterthought bolted onto auth β€” it is auth, and it's where the previous three courses' attacks converge. Secure it last in the course, but never last in priority.

Hands-On Exercises

Exercise 1

Explain why account recovery is effectively an authentication bypass and must be as strong as login. Then design a secure password-reset flow and list the five required token properties (entropy, hashing, expiry, single-use, account-binding) with the reason for each.

πŸ“„ View solution
Exercise 2

Identify and fix the bug in each reset implementation: (a) the token is the user's ID times a timestamp; (b) the reset link never expires and works repeatedly; (c) the reset URL's domain comes from the request Host header. Explain the attack each enables.

πŸ“„ View solution
Exercise 3

Walk through the CSRF course's "change email β†’ password reset β†’ takeover" chain, then list the recovery-side defences from this chapter that break it at each step (re-authentication, old-address notification, generic responses + rate limiting, session invalidation). Explain why each step is closed.

πŸ“„ View solution

Chapter 9 Quick Reference

  • Recovery is an authentication bypass by design β€” attackers hit the weakest path, so secure it as strongly as login (the #1 ATO vector)
  • Reset flow: generic response β†’ CSPRNG token (store hashed, short expiry) β†’ emailed single-use link β†’ set new password β†’ invalidate token + sessions + notify
  • Token properties: high-entropy Β· hashed at rest Β· short expiry Β· single-use Β· bound to one account
  • Classic bugs: predictable tokens Β· non-expiring/reusable tokens Β· Host-header link poisoning (build URLs from a trusted base, not the request host)
  • Avoid security questions β€” public/guessable knowledge factors
  • Email verification uses the same token mechanism; keep responses generic (anti-enumeration)
  • Sensitive changes: re-authenticate, notify (old address too on email change), invalidate sessions on reset, optional change cool-down
  • This closes the CSRF takeover chain β€” recovery is where the prior courses' attacks converge; it IS auth, not an afterthought
  • Next chapter: testing, pitfalls & a hardening checklist β€” the course finale
Chapter 10 of 10

Testing, Pitfalls & Hardening Checklist

Chapter 10
Testing, Pitfalls & Hardening Checklist
Auth testing, the broken-auth catalogue, and a deployable checklist

The finale turns the course into practice: how to test an authentication system, the broken-auth patterns to recognize, and a single deployable checklist that pulls all ten chapters β€” and the whole HTTPS/CSRF/XSS quartet β€” together.

How to Test Authentication

Auth testing is mostly about probing each part of the lifecycle for the weaknesses each chapter named:

  • Password storage β€” confirm hashes are slow + salted (bcrypt/argon2), not MD5/SHA; that you never store or log plaintext.
  • Login β€” try brute force / stuffing against rate limits; check for user enumeration via messages and timing; confirm generic errors.
  • Session β€” verify the ID is high-entropy & regenerated at login (fixation); check cookie flags; test idle/absolute timeout and that logout truly invalidates server-side.
  • MFA β€” confirm it can't be skipped by manipulating the flow; test recovery-code handling; check for MFA-fatigue protections.
  • Tokens β€” try alg:none and algorithm swaps; tamper with claims; check token expiry/revocation actually works.
  • Recovery β€” test token entropy/expiry/single-use, Host-header poisoning, and that sensitive changes re-authenticate.
Reference the standards β€” don't test from memory
Two free references make auth testing systematic: the OWASP Authentication Cheat Sheet (and Session Management, Password Storage, Forgot Password cheat sheets) for the "what good looks like," and the OWASP ASVS (Application Security Verification Standard) chapters on authentication and session management for a checklist of verifiable requirements at increasing rigor levels. OWASP ZAP / Burp Suite automate parts (session-token analysis, brute-force, tampering), but the highest-value testing is manually walking each flow against these references. Auth bugs are logic bugs β€” scanners miss most of them.

The Broken-Auth Catalogue

Broken practiceWhy it failsChapter
Plaintext / fast-hash / encrypted passwordsbreach = mass plaintext recovery2
No rate limiting / naive lockoutbrute force / stuffing; self-DoS3
User enumeration (message or timing)lets attackers target real accounts3
Predictable session IDssession hijacking by guessing4
No session regeneration at loginsession fixation5
Missing cookie flags / cookie-only logouttheft, riding, stale sessions5
SMS-only MFA / weak recovery fallbackSIM-swap; downgrades strong MFA6
alg:none / unverified JWTforged tokens, identity spoofing7, 8
OAuth without state / loose redirect_uricallback CSRF, code theft8
Weak password reset (predictable/eternal/poisonable)account takeover via recovery9

The Hardening Checklist

β˜‘
Passwords: slow salted hash (Argon2id / bcryptβ‰₯12); never plaintext/encrypted; upgrade-on-login; breach-check new passwords
β˜‘
Login: rate-limit by IP and account; backoff over hard lockout; generic responses + uniform timing (no enumeration)
β˜‘
MFA: offer TOTP/passkeys; avoid SMS-only; number-matching for push; recovery codes stored hashed
β˜‘
Sessions: high-entropy IDs; regenerate at login; idle + absolute timeout; logout destroys server-side session
β˜‘
Cookies: HttpOnly + Secure + SameSite (+ __Host-) on the session cookie
β˜‘
Tokens: pin the JWT algorithm (no none); short access + revocable/rotating refresh; verify ID-token sig/iss/aud
β˜‘
OAuth/OIDC: auth-code + PKCE; verify state; exact redirect_uri; use OIDC ID token for identity
β˜‘
Recovery: CSPRNG token, hashed, short expiry, single-use; trusted base URL; re-auth + notify on sensitive changes; invalidate sessions on reset
β˜‘
Transport: HTTPS-only + HSTS (login & everything); the credential/session never on plain HTTP
β˜‘
Don't roll your own: use vetted libraries/frameworks/identity providers; configure them strictly
The principle that survives the whole course
If one idea outlasts the details: the entire account-security problem is protecting one secret β€” the credential, then the session that stands in for it β€” across its whole life. Store it so a breach can't reverse it (hashing), prove it without resending it (sessions/tokens), keep it confidential in transit (HTTPS) and at rest (HttpOnly/Secure), stop it being guessed (rate limits/MFA), ridden (CSRF/SameSite), stolen (XSS/HttpOnly), or bypassed (recovery hardening). Every chapter is one facet of guarding that secret β€” and every defence assumes the others hold, which is why account security is only as strong as its weakest link.

How the Course (and Quartet) Fits Together

The arc: establish identity (Ch. 1 foundations, Ch. 2 password storage, Ch. 3 login defences), carry it (Ch. 4 sessions, Ch. 5 session security), strengthen it (Ch. 6 MFA), scale it (Ch. 7 tokens, Ch. 8 OAuth/OIDC), recover it safely (Ch. 9), and verify it all (Ch. 10). And it completes the quartet: HTTPS protects the secret in transit, Authentication establishes and holds it, CSRF stops it being ridden, XSS stops it being stolen. Four courses, one secret, defended end to end.

Hands-On Exercises

Exercise 1

Write an authentication test plan covering password storage, login, session, MFA, tokens, and recovery β€” naming the specific weakness to probe in each area and how you'd test it (e.g. timing-based enumeration, session-ID regeneration at login, alg:none).

πŸ“„ View solution
Exercise 2

Audit this app against the broken-auth catalogue: it stores SHA-256 passwords, has no login rate limit, keeps the same session ID after login, stores JWTs in localStorage, and emails non-expiring reset links built from the Host header. List every flaw, its chapter, and the fix.

πŸ“„ View solution
Exercise 3

Produce a prioritized hardening plan for a new web app's auth (cookie sessions, password + optional MFA, email password reset). Order the measures, justify the ordering, and show how the four security courses (HTTPS / Auth / CSRF / XSS) each contribute to protecting the session.

πŸ“„ View solution

Chapter 10 Quick Reference

  • Test each lifecycle stage for its named weakness; auth bugs are logic bugs scanners miss β€” walk flows manually against OWASP cheat sheets / ASVS
  • Broken-auth catalogue: weak hashing Β· no rate limit Β· enumeration Β· predictable/un-regenerated session IDs Β· missing cookie flags Β· SMS-only MFA Β· alg:none Β· OAuth without state Β· weak reset
  • Checklist: slow salted hashes Β· IP+account rate limits Β· MFA (TOTP/passkeys) Β· regenerate session at login Β· HttpOnly/Secure/SameSite Β· pin JWT alg + verify ID token Β· auth-code+PKCE Β· secure reset Β· HTTPS+HSTS Β· don't roll your own
  • The one principle: protect a single secret (credential β†’ session) across its whole life β€” stored, proven, in transit, guessed, ridden, stolen, recovered
  • Account security = the weakest link across all of it; each defence assumes the others hold
  • Completes the quartet: HTTPS (in transit) Β· Auth (establish/hold) Β· CSRF (not ridden) Β· XSS (not stolen)

β˜… Authentication & Session Security Complete β€” 10 / 10 chapters

From AuthN-vs-AuthZ foundations through password hashing, login defences, sessions and their security, MFA, tokens and JWTs, OAuth/OIDC, secure recovery, and a deployable hardening checklist. Paired with HTTPS, CSRF, and XSS, you now hold the complete quartet of web account security β€” four courses defending one secret end to end: stored irreversibly, proven without resending, kept confidential, and never guessed, ridden, stolen, or bypassed.