SessionAuthentication, TokenAuthentication, and JWT
coreintermediateAn authentication class answers one question: who sent this request? SessionAuthentication reads Django's own session cookie, so it works for a browser front-end already logged in through the normal Django login page — and because it rides on a cookie, DRF still enforces CSRF on unsafe methods. TokenAuthentication reads an `Authorization: Token <key>` header and looks that key up in a database table, which suits a mobile app or a script that has no cookie jar. A JWT is a token whose contents are signed rather than stored: the server can check it with a signing key alone, so no database row is read per request — but nothing revokes it before it expires, which is why Simple JWT issues a short-lived access token alongside a longer-lived refresh token.
Think of it as
The three schemes differ on one axis: where the truth about "who is this" lives. A session keeps it server-side and hands the client an opaque pointer (the cookie) — revoking a login is deleting one row, and the browser attaches the cookie automatically, which is exactly why CSRF becomes a concern. A DRF token also keeps it server-side, but the client sends it deliberately in a header instead of the browser sending it automatically, so CSRF does not apply — the cost is a database lookup on every request and a token that never expires unless you build expiry yourself. A JWT moves the truth into the token itself: the payload carries the user id and an expiry, and a signature proves the server minted it, so a request can be authenticated with zero database access. That is the whole trade — you buy statelessness and pay for it with revocation, because a signed token stays valid until its `exp` passes no matter what happens to the account behind it. Short access-token lifetimes are the standard mitigation, not a detail.
What we're doing: Wire Simple JWT globally, expose the token endpoints, and keep session auth for the browsable API.
- 3–4
- Order matters — DRF tries each class in turn and stops at the first that returns a user. JWT first means an API client with a Bearer header never touches the session machinery.
- 10
- Fifteen minutes is the revocation window: a disabled account keeps working for at most that long, because nothing checks the database while an access token is still inside its lifetime.
- 12
- ROTATE_REFRESH_TOKENS issues a fresh refresh token on every refresh, so a stolen refresh token is usable only until the real client refreshes next.
Why this works: Keeping SessionAuthentication second costs nothing for API clients (it is only reached when no Bearer header is present) and keeps DRF's browsable API usable while logged into the admin — a real convenience during development that does not weaken the API path.
Storing a JWT in `localStorage` and treating the payload as private
Wrong
Better
What you see: Any script that runs on the page — an injected one, a compromised dependency — reads `localStorage.jwt` and replays it, and anyone who obtains the token can also base64-decode the middle segment and read every claim in it verbatim.
Why: A JWT signature guarantees the server minted the token; it guarantees nothing about confidentiality, because the payload is base64url — an encoding, not encryption. Two consequences follow: claims are public to whoever holds the token, and the token is a bearer credential, so wherever it is stored is exactly as sensitive as a password. `localStorage` is readable by any JavaScript in the origin, which makes an XSS bug into a full account takeover that outlives the page.
- Incoming request
- Session — state on the server, pointer in a cookie
- Cookie holds a session key — the browser attaches it automatically
- Row in django_session — one SELECT per request
- CSRF token required — unsafe methods only, authenticated requests only
- Revoke = delete the row — takes effect on the next request
- DRF Token — state on the server, key in a header
- Authorization: Token <key> — sent deliberately, never automatically
- Row in authtoken_token — one SELECT per request
- No CSRF exposure — a cross-site form cannot set a header
- No expiry built in — you add rotation yourself
- JWT — state inside the token itself
- Authorization: Bearer <jwt> — header.payload.signature
- Signature check only — zero database reads
- Payload is readable — signed, not encrypted — never put secrets in it
- Valid until exp — a deleted account still authenticates until then
Choosing a scheme
Together
Remember: Session = server-side state behind an automatic cookie, so CSRF applies. Token = server-side state behind a deliberate header, so CSRF does not, but every request costs a database read. JWT = the state is in the signed token, so no database read at all — and no revocation before `exp`, which is why access tokens live for minutes and a refresh token does the long-lived work. A JWT payload is signed, not encrypted: never put anything secret in it.
See also: authentication order anonymous users and failures · login logout and authentication backends · cookie security and csrf

