Exercise 1 — Login
1-2. The correct flow: login → {access, refresh}; API with Bearer → 200; no header → 401 credentials_not_provided; expired → 401 token_not_valid (SimpleJWT answers 401 with the code in the body — your frontend uses it to silently refresh and retry once).
- After
refresh, the old pair is blacklisted (BLACKLIST_AFTER_ROTATION): the old refresh gives 401. This is what a real logout does (Ex. 5).
Exercise 2 — Anatomy
- Default claims:
token_type,exp,iat,jti,user_id. No email (good: least privilege). Payment data NEVER: the payload is base64 readable by anyone holding the token — the signature protects integrity, not confidentiality. - One changed character → 401
token_not_valid(invalid signature: the payload's HMAC no longer matches). - If the role traveled in the payload and someone edited it to ADMIN, the signature would not revalidate (the HMAC would change): the server rejects. The real risk of the role in the token is not editing but information staleness: a role changed in the DB stays alive in issued tokens until they expire — that is why the role is read from the DB on every request, not from the token.
Exercise 3 — Where it lives
- localStorage: the XSS reads the whole token and exports it (full compromise until expiry). HttpOnly cookie:
document.cookiedoesn't show it — the XSS can USE the session from the victim's browser while it lives, but cannot export the token nor persist access after closing. Memory + HttpOnly refresh: it steals only the live access; the refresh (7 days) is protected. - SameSite=Lax blocks sending the cookie on cross-site POSTs (curl with a fake Origin is NOT a browser: it doesn't run SameSite — the protection lives in the browser; the real attacker is the user's browser, and there Lax cuts it). Django's CSRF token remains for the admin/classic forms.
- Summary: 10-min access in memory; 7-day refresh in a rotated HttpOnly cookie + blacklist; mobile: the OS keychain.
Exercise 4 — ADR-0008 (skeleton)
Context: SPA web + mobile over the same API; revocable payments; compliance. Decision: short JWT access (10m) + rotated refresh (7d) with blacklist; refresh in an HttpOnly SameSite=Lax cookie on web, keychain on mobile. Alternatives: Django session only (mobile friction, state scaling), long JWT in localStorage (total XSS). Consequences: blacklist infrastructure (Redis/DB), logout = blacklist, mandatory rotation, refresh reuse detected = revoke the family (theft detection).
Exercise 5 — Real logout
- Blacklist the refresh:
refresh.blacklist()(or SimpleJWT's TokenBlacklist endpoint). The user no longer gets new accesses: effective logout. - The live access (≤10 min) stays valid: acceptable in most threat models (a short window). If not: blacklist by
jtichecked on every request — you pay one lookup (Redis) per request: part of the state the JWT avoided comes back. A conscious decision: which exposure window you accept.
Professor's summary
- State doesn't disappear with JWT: it moves (blacklist, rotation). The question is what you revoke and how fast.
- Short access + rotated HttpOnly refresh + blacklist: the defensible default pattern in 2026.
- Readable JWT ≠ secret; signature ≠ encryption. Minimal payload.
- Role and sensitive data get queried from the DB per request; the token only identifies.
Next: Lesson 19 — OAuth2 and OpenID Connect.