Module 4 · Authentication and security

Lesson 18 — Sessions vs tokens

JWT and its risks, refresh tokens and what fits each client best.

Published
In this lesson
  1. Exercise 1 — Login
  2. Exercise 2 — Anatomy
  3. Exercise 3 — Where it lives
  4. Exercise 4 — ADR-0008 (skeleton)
  5. Exercise 5 — Real logout
  6. Professor's summary

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).

  1. 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

  1. 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.
  2. One changed character → 401 token_not_valid (invalid signature: the payload's HMAC no longer matches).
  3. 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

  1. localStorage: the XSS reads the whole token and exports it (full compromise until expiry). HttpOnly cookie: document.cookie doesn'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.
  2. 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.
  3. 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

  1. Blacklist the refresh: refresh.blacklist() (or SimpleJWT's TokenBlacklist endpoint). The user no longer gets new accesses: effective logout.
  2. The live access (≤10 min) stays valid: acceptable in most threat models (a short window). If not: blacklist by jti checked 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.