Module 4 · Authentication and security

Lesson 20 — Password hashing and MFA

bcrypt/argon2, MFA and account recovery without opening the door to attackers.

Published
In this lesson
  1. Exercise 1 — The hash in your hand
  2. Exercise 2 — Enumeration and messages
  3. Exercise 3 — Full TOTP
  4. Exercise 4 — Recovery codes
  5. Exercise 5 — Secure reset
  6. Exercise 6 — The factor map
  7. Exercise 7 — The side-door pentest
  8. Submit

pip install django[argon2] pyotp qrcode. Don't look at solutions.md before submitting.

Exercise 1 — The hash in your hand

  1. Change PASSWORD_HASHERS so Argon2id is first. Create a new user and look at their hash in the shell (user.password): identify the algorithm, m (memory), t (iterations), p (parallelism) and the salt.
  2. Measure with time.perf_counter(): check_password("xxx", hash) 5 times. How long does one verification take? How many attempts/second would an attacker with 1 thread make? And 10,000 GPUs? (reason about the order of magnitude).
  3. Double bcrypt's iterations or argon2's memory_cost: re-measure. What do you pay (login) and what do you gain (brute force)?
  4. Add django-pwned-passwords as a validator: try to register with "password123" and capture the response.

Exercise 2 — Enumeration and messages

  1. In your login: log in with a nonexistent email and with a wrong password. Do the times differ? The messages? Equalize both ("Invalid credentials") and justify it.
  2. Try ?user=admin on your forgot-password: does the response leak whether it exists? Fix it to a single response.

Exercise 3 — Full TOTP

  1. Model MFA(user OneToOne, secret_encrypted, confirmed bool) + an enrolment view: generate the secret, render the QR (qrcode → inline SVG), confirm with a valid code.
  2. Login with MFA: if mfa.confirmed, the login returns {"mfa_required": true} instead of tokens; a second endpoint verifies the TOTP code and ISSUES tokens (18). Demonstrate it with the full flow.
  3. Clocks: use valid_window=1 and explain what time window you accept with that.

Exercise 4 — Recovery codes

  1. On MFA confirmation: generate 10 codes (secrets.token_hex(4) formatted), show them ONCE, store each one's hash (check_password) and used_at NULL.
  2. Allow login with a recovery code when no TOTP is at hand: constant-time verification + mark used_at in the same transaction (what does using the same code twice at once prevent? connect it with 10).
  3. A counter of remaining codes in the profile + a warning at 3.

Exercise 5 — Secure reset

  1. Implement the full flow: POST /api/auth/forgot/ (always the same response) + a simulated email (print the link) + POST /api/auth/reset/ with token+new password.
  2. Verify: a token used twice → the second is rejected; a 2h-old token → rejected; after a successful reset, all the user's refresh tokens are invalidated (18).
  3. For an account with MFA: what does your reset demand? Implement at least demanding a recovery/TOTP code when using the link.

Exercise 6 — The factor map

  1. Classify the factors your TicketFlow supports TODAY in the §5 table and decide which you would add: passkeys with django-webauthn, or hardening recovery? Write the decision with its reason (49: the trade-off). Note: if you choose passkeys, the minimum viable is registering one credential (public key) and verifying the challenge at login.
  2. Session rotation: implement that a password reset (and an MFA change) invalidates ALL the user's active sessions (18: the refresh blacklist + jti). Test: login on 2 devices → reset from device 1 → device 2's refresh fails.

Exercise 7 — The side-door pentest

  1. List ALL the ways back into a TicketFlow account (password, email reset, recovery codes, human support, changing the profile's email). For each: what does it demand? Is any weaker than the rest?
  2. The support protocol: write (48: the runbook) the 5 steps human support follows for "I lost everything": what they verify, what they NEVER do (a reset over the phone?), and what they record in the audit log (56).
  3. The reset enumeration test: automate 20 reset requests (10 with existing emails, 10 invented): are the response and timing identical in both groups? Measure both groups' response time: is the difference detectable? (if yes: the flow does uneven work depending on the email's existence — the timing-attack finding).

Submit

Paste hashes (trimmed), timings and the QR flow. Next: Lesson 21 — Authorization.