What you'll learn
Quick Answer
Sessions store state on the server and are easy to revoke. JWTs are self-contained and need no lookup, but stay valid until they expire. Store either in an HttpOnly cookie rather than localStorage, and keep token lifetimes short.
Authentication is not authorisation
Worth separating, because interviews ask and the words get used interchangeably.
Authentication is proving who you are — logging in. Authorisation is what you are permitted to do once identified. A logged-in user is authenticated; whether they may delete someone else's post is authorisation.
The corresponding status codes reflect this: 401 means not authenticated, 403 means authenticated but not allowed.
A common and serious bug is checking only the first. Hiding a delete button in the UI is not authorisation — the endpoint must verify the requester owns the resource, because anyone can call it directly.
Sessions: state on the server
The traditional approach. On login the server creates a session record, stores it, and sends the client a session ID in a cookie. On each request it looks the ID up.
Set-Cookie: sid=8f3a...; HttpOnly; Secure; SameSite=Lax
The cookie holds only an opaque identifier — no user data. Everything meaningful stays server-side.
Advantages. Revocation is trivial: delete the record and the session is dead immediately. You can list active sessions, force logout everywhere, and change permissions that take effect on the next request.
Costs. The server stores state, so a lookup happens per request, and multiple servers must share that store — typically Redis. That is the scaling objection, though it is a smaller problem in practice than it is often presented as.
JWTs: state in the token
A JSON Web Token carries the data itself, signed so it cannot be altered. Three base64 parts separated by dots: header, payload, signature.
{ "sub": "user_42", "role": "student", "exp": 1755000000 }
The server verifies the signature with its secret and trusts the contents — no database lookup required, which is the main appeal.
The critical misunderstanding: a JWT is signed, not encrypted. Anyone holding it can read the payload; paste one into a decoder and the claims are plainly visible. Signing prevents modification, not reading. Never put anything private in a JWT.
The real cost is revocation. Because the server keeps no record, a valid unexpired token is accepted. If it is stolen, or the user's role is downgraded, or they log out — the token still works until it expires. Fixing that requires a blocklist checked on every request, which reintroduces the server-side lookup JWTs were adopted to avoid.
The standard mitigation is short-lived access tokens, around fifteen minutes, plus a longer-lived refresh token that is stored and revocable. That works well and is more machinery than most tutorials show.
Where to store it, which is the security decision
This matters more than sessions versus JWTs.
localStorage is readable by any JavaScript on the page. A cross-site scripting flaw, or one compromised npm dependency, can read the token and send it elsewhere. Convenient, and it is why so many tutorials use it.
An HttpOnly cookie cannot be read by JavaScript at all. Script can still trigger requests that carry it, but it cannot exfiltrate the token itself — which is a meaningful reduction in blast radius.
Use cookies with all three flags: HttpOnly so script cannot read it, Secure so it is sent only over HTTPS, and SameSite to limit cross-site request forgery.
The cookie approach does need CSRF protection, because cookies are sent automatically on cross-site requests. That is a well-understood problem with standard solutions, and it is a better trade than exposure to token theft via XSS. See localStorage, sessionStorage and cookies.
Practical rules
- Never store passwords in a recoverable form. Hash with bcrypt or Argon2, which are deliberately slow. A plain SHA-256 is far too fast and unsuitable for passwords.
- Keep the signing secret out of the repository. Environment variables, and rotate anything ever committed.
- Always verify the signature server-side, and reject tokens whose header specifies
noneas the algorithm — an old but recurring vulnerability. - Check expiry. A library usually does; confirm rather than assume.
- Do not invent your own scheme. Use an established library, or a provider, for anything real.
- Rate-limit login endpoints, or you have built a password-guessing service.
For a student project, sessions are usually simpler and safer than JWTs, and being able to explain why you chose them — revocation, no client-side storage decision — is a stronger interview answer than having implemented JWTs from a tutorial.
