pip install django[argon2] pyotp qrcode. Don't look at solutions.md before submitting.
Exercise 1 — The hash in your hand
- Change
PASSWORD_HASHERSso 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. - 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). - Double bcrypt's
iterationsor argon2's memory_cost: re-measure. What do you pay (login) and what do you gain (brute force)? - Add
django-pwned-passwordsas a validator: try to register with "password123" and capture the response.
Exercise 2 — Enumeration and messages
- 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.
- Try
?user=adminon your forgot-password: does the response leak whether it exists? Fix it to a single response.
Exercise 3 — Full TOTP
- 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. - 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. - Clocks: use
valid_window=1and explain what time window you accept with that.
Exercise 4 — Recovery codes
- On MFA confirmation: generate 10 codes (
secrets.token_hex(4)formatted), show them ONCE, store each one'shash(check_password) andused_atNULL. - 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).
- A counter of remaining codes in the profile + a warning at 3.
Exercise 5 — Secure reset
- 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. - 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).
- 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
- 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.
- 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
- 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?
- 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).
- 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.