Authentication overview
AuthN is who you are; AuthZ is what you may do. They map to 401 and 403, they fail differently, and conflating them is the most common design confusion in this area. Everything below assumes that split.
The mechanisms, and what each is for
| Mechanism | Carries | Revoke |
|---|---|---|
| Session cookie | an opaque id | delete the row |
| JWT | signed claims | wait, or blocklist |
| API key | a long-lived secret | rotate |
| OIDC | delegated identity | at the IdP |
| mTLS | a client certificate | revoke the cert |
The column that decides most arguments is the last one.
Session or token
# Session: the server holds the truth.
session_id = secrets.token_urlsafe(32)
await redis.setex(f"sess:{session_id}", 3600, user.id)
# Logout is one DEL. Effective immediately, everywhere.# JWT: the token holds the truth.
token = jwt.encode(
{"sub": user.id, "exp": now + 900}, KEY, algorithm="RS256",
)
# Logout is... nothing. It is valid until it expires.That is the whole trade. Sessions need a shared store and cost a lookup per request; tokens need neither and cannot be withdrawn.
The usual production answer is both: a short-lived access token (minutes) so a stolen one expires quickly, plus a long-lived refresh token stored server-side so it can be revoked. You get statelessness on the hot path and revocation where it matters.
Gotcha: “stateless JWT” and “log out everywhere now” are incompatible by construction. If a requirement says instant revocation, you are keeping server state — the only question is whether it is a session table or a blocklist.
Passwords
from argon2 import PasswordHasher
ph = PasswordHasher() # sane defaults, salt included
stored = ph.hash(password) # $argon2id$v=19$m=65536,t=3,p=4$...
try:
ph.verify(stored, attempt)
except VerifyMismatchError:
...The salt is inside the hash string; you do not manage it. What matters:
- A slow adaptive hash — Argon2id, scrypt or bcrypt. Never SHA-256, which is fast, which is precisely the wrong property.
- Constant-time comparison, which
verifydoes. A naive==leaks length and prefix through timing. - Rehash on login when the cost parameters have moved on —
ph.check_needs_rehash(stored).
See Password hashing.
OAuth 2.0 is not authentication
This is the distinction interviewers probe. OAuth 2.0 answers “may this app access that resource”. It says nothing about who the user is — an access token is a bearer credential, not an identity claim.
OIDC adds the identity layer: an ID token, a JWT with sub, iss, aud
and an expiry, meant for your application to consume. Using a raw OAuth access
token as proof of identity is a real vulnerability, because a token minted for
another application will happily be presented to yours.
The flow to know is authorization code with PKCE, now the default for every client type:
verifier = secrets.token_urlsafe(64)
challenge = b64url(sha256(verifier.encode()).digest())
# 1. Redirect with the challenge.
# ...?code_challenge={challenge}&code_challenge_method=S256
# 2. Get a code back.
# 3. Exchange it, proving you hold the verifier.
await post(TOKEN_URL, data={
"grant_type": "authorization_code",
"code": code, "code_verifier": verifier,
})Without PKCE, an attacker who intercepts the redirect can exchange the code themselves. With it, the code is useless without the verifier, which never left the client.
See SSO and OIDC and OAuth 2.0 Flows.
Least privilege
Minimum roles, minimum scopes, resource-level checks, and a review cadence. The
practical version for an API: authorise against the resource, not just the
role. is_admin is not an answer to “may this user see this document” — see
the object-permission trap in
Django REST Framework (DRF).
Interview angle 6
- “Authentication versus authorisation?” - authentication establishes identity, authorisation determines permission. They map to 401 and 403 respectively, and conflating them is the most common design confusion in this area.
- “Session or token?” - sessions are server-side state, revocable instantly, and need a shared store. Tokens are stateless and verifiable anywhere but cannot be revoked before expiry. Production usually runs both: a short access token plus a revocable refresh token.
- “How do you store passwords?” - a slow adaptive hash with a per-user salt: Argon2id, scrypt or bcrypt, verified in constant time. Never a general-purpose hash like SHA-256, which is fast and therefore brute-forceable.
- “Is OAuth 2.0 authentication?” - no. It authorises access to a resource and says nothing about identity. Treating an access token as proof of who the user is accepts a token minted for a different application. OIDC adds the ID token for that.
- “What does PKCE protect against?” - interception of the authorization code. The client proves it holds a secret verifier it never transmitted, so a stolen code cannot be exchanged by anyone else. It is now the default for confidential clients too, not just mobile.
- “How do you handle logout with JWTs?” - either accept the token stays valid until it expires, and keep expiry short, or keep a server-side blocklist. There is no third option: instant revocation means server state by definition.