Exercise 1 — The hash
- Argon2 format:
argon2id$v=19$m=65536,t=3,p=4$<salt>$<digest>— 64MB of memory, 3 iterations, 4 threads. The salt is included (random per user). - Typically 50-200ms per verification on a laptop → ~5-20 verifications/sec/thread. An offline attacker with 10,000 GPUs (each with its GBs): thousands of times faster, but argon2's memory cost limits the real parallelism per GPU — that is argon2id's design (CPU+memory together raise the cost of mass cracking).
- Doubling parameters roughly doubles your login time (100→200ms) and more than doubles the attacker's cost per attempt (theirs scales per attempt, yours per legitimate login). The balance: ~100-250ms of login is imperceptible; tune it on real hardware (benchmark on production-like machines).
- Pwned Passwords with k-anonymity: you send only the first 5 characters of the SHA-1 (the server never sees your password) and compare the rest locally — a validator that rejects known-breached passwords.
Exercise 2 — Enumeration
- Times differ if the flow cuts early (email doesn't exist → you don't compute the hash): an attacker measures and enumerates emails. Equalizing: do the same work (a dummy hash) or sleep: same response, roughly the same time. A single message: "Invalid credentials".
- The forgot endpoint's single response: "If an account exists, you will receive an email" whether or not it does. Timing filtering applies here too (the same work always).
Exercise 3 — TOTP
- QR with
otpauth://totp/TicketFlow:{email}?secret={b32}&issuer=TicketFlow: the user's app stores the secret; your DB stores yours (encrypted).confirmedprevents activating MFA without proving the user scanned correctly. - Flow: login ok →
mfa_requiredwith no tokens →POST /auth/mfa/verifywith the code → tokens issued. Without this intermediate step, the password alone would be enough and the MFA decorative. valid_window=1accepts the previous/next period's code (±30s): it covers mild clock drift without opening the door to codes from minutes ago (with replay: the same code could be verified twice inside the window — a Redis/DB "last used counter" per user prevents it; pyotp doesn't do it for you).
Exercise 4 — Recovery codes
- 10 codes
xxxx-xxxx; in the DB ONLY the hash (like passwords): if the DB leaks, the codes are unusable. - Single use under concurrency:
UPDATE... SET used_at = now() WHERE id = X AND used_at IS NULL(one row, affects 0 or 1 — lesson 10 applied): two simultaneous uses, one wins. The hash check comes first; the atomic UPDATE of the flag decides. - A visible counter + regenerate (invalidates the old ones) + a warning: the user doesn't discover they have no codes the day they lose their phone.
Exercise 5 — Reset
- The same response always; the real email only if it exists. Token:
token_urlsafe(32), hashed in DB, TTL 1h, single-use (the same technique as Ex. 4). - Second use of the token: rejected (used_at). Expired token: rejected (exp). After the reset: blacklist all the user's refresh tokens (18) + notification "your password changed" (if it wasn't you, contact us) — the attacker who already had a session loses access.
- With MFA: the email link is NOT enough — demand a TOTP or recovery code when using the token. Without this, the attacker controlling the victim's email resets and walks in: the reset is the spot that BYPASSES the hash, and that is why it cannot bypass the MFA.
Professor's summary
- argon2id with memory: the slow hash turns a DB leak into a problem of years, not hours.
- Don't enumerate users (identical timings and messages); let HIBP say which passwords are burned.
- TOTP: encrypted secret, an honest window, hashed single-use recovery codes under a transaction.
- The password reset is the door that bypasses everything: demand the second factor and revoke sessions.
Next: Lesson 21 — Authorization.