Server-side auth
Session cookies vs JWT: what you're actually trading
Authentication proves who you are. Authorization decides what you may do. Keep them apart in your head, because the token questions all live on the first and most access bugs live on the second.
A session cookie is an opaque id. The server holds the real state (in Redis or a table) and looks it up on every request. Revoking a session is one DELETE and it takes effect on the next request. The price is that lookup: your session store now sits on the hot path and inside your availability budget.
A JWT moves the state into the token. The server verifies the signature and the exp claim locally, with no lookup, so any node can validate any request. That is why JWTs scale horizontally without shared state. The cost arrives the moment you want to kick someone out.
How a server validates a JWT (and the logout gap)
A JWT is three base64url segments: header, payload, signature. It is signed, not encrypted, so treat the payload as public.
wrong "the token is encrypted, so it's fine to stash the user's email and role in it." Anyone holding the token can decode the payload in a browser console. Signing protects integrity; it does nothing for confidentiality.
Validation is more than a signature check. Verify the signature with the algorithm you expect, then check exp, iss, and aud. Skipping those last three is a documented vulnerability class.
Then the gap. A valid JWT stays valid until exp. There is no server row to delete, so "log this user out right now" has no direct answer unless you built one.
Refresh tokens, without the hand-waving
The standard shape is a short-lived access token (5 to 15 minutes) plus a long-lived refresh token. The access token dies fast, so a leak has a narrow window. When it expires the client exchanges the refresh token at the token endpoint for a new access token. Rotation means every refresh also hands back a fresh refresh token and retires the old one, so a replayed old token is a theft signal you can act on.
Refresh tokens differ from access tokens by more than lifetime. They hit a different endpoint, carry a different audience, and never go to your resource APIs.
Where to keep any of this in a browser, plus CSRF and cookie flags, belongs to web-security. On the server, TTL and rotation are the two levers you actually turn.
Making a JWT forgeable: alg confusion and alg:none
A signature is only as strong as how you verify it. Two classic breaks, both catalogued in RFC 8725.
alg: none: the token declares no algorithm and ships an empty signature segment. A permissive library reads that as "already valid." Reject it outright.
Algorithm confusion is subtler. Your service issues RS256, where a private key signs and a public key verifies. An attacker re-signs a tampered token as HS256 (symmetric) using your public key as the HMAC secret. If your verify call takes the algorithm from the token's own header, it happily runs an HMAC check with a key the attacker also holds.
// WRONG: algorithm comes from attacker-controlled input
jwt.verify(token, key, { algorithms: [decodedHeader.alg] });
// RIGHT: pin the algorithm you actually issue
jwt.verify(token, publicKey, { algorithms: ["RS256"] });
Rotate signing keys, select the verify key by kid, and publish a JWKS endpoint so resource servers fetch public keys instead of sharing a secret.
When "stateless" quietly becomes stateful
Instant revocation of a JWT means storing revoked jti values and checking them per request. Keep them in Redis with a TTL equal to the token's remaining exp, so the set stays bounded and self-cleans.
wrong "we picked JWTs precisely so we never touch a store on the request path." The moment that denylist exists you are doing a lookup on every request, which is the job a session store already did. If revocation latency matters to you, price out plain sessions before bolting a denylist onto stateless tokens.
Picking an OAuth2 grant, and what OIDC adds
Choose the grant by client type, not habit. Authorization code with PKCE for anything a human logs into. Client credentials for machine-to-machine. Device code for TVs and CLIs. OAuth 2.1 dropped the implicit and password grants and made PKCE mandatory for the code flow, so a fresh design leaning on implicit reads as dated.
OAuth2 is authorization. OIDC layers authentication on top and returns an id_token, a JWT proving who signed in, alongside the access token. The id_token is for your client to read; the access token is what the resource API validates. Handing an access token to an app as proof of identity is the "OAuth as login" anti-pattern.
RBAC, ABAC, and the authorization bug that keeps shipping
RBAC attaches permissions to roles and users to roles. It covers most apps and then buckles on row-level questions like "can this user edit this document," where you either explode roles per resource or move to ABAC: a policy evaluated over subject, resource, action, and environment attributes. Most teams settle on coarse RBAC at the door with finer checks below it.
Whatever model you pick, enforce it at the data layer. The bug that keeps reaching production is IDOR: a request is authenticated, clears the role check, and reads /orders/1043 belonging to someone else because nothing verified ownership of that id.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.