Sessions, tokens, and what to use when
Authentication is the part you cannot afford to improvise. This lesson is the decision that shapes the rest of the module: how the server remembers who a request is from.
The problem
HTTP is stateless. Every request arrives with no memory of the last one, so after a successful login the next request has to prove who it is somehow.
Two approaches.
Sessions
The server keeps the state:
1. User logs in
2. Server creates a session, stores it (database or Redis), gets an id
3. Server sends the id in a cookie
4. Browser sends the cookie automatically on every request
5. Server looks the id up and knows who it is
The cookie holds a meaningless random id. Everything real is on the server.
Advantages:
You can revoke instantly. Delete the session row and that login is dead — for a "sign out everywhere" button, or the moment you detect something wrong.
Nothing sensitive leaves the server. The id reveals nothing even if stolen from a log.
You can change what a session means — promote a user to admin and it takes effect on their next request.
Disadvantages:
The server holds state, so every request reads the session store. That means a shared store across instances.
It is awkward across domains and not naturally suited to a mobile app.
JWTs
The server keeps nothing:
1. User logs in
2. Server creates a signed token containing the user id and role
3. Client stores it and sends it on every request
4. Server verifies the signature and reads the claims
A JWT is three base64 parts: header, payload, signature. The signature proves the server issued it and that nobody edited it.
A JWT is signed, not encrypted. Anyone can read the payload — paste one into jwt.io and it decodes. Never put anything secret in a token. A user id and a role are fine; an email address is borderline; anything private is not.
Advantages:
Stateless. Verification is a signature check, no lookup. That scales across instances trivially.
Works anywhere — mobile apps, other services, across domains.
Disadvantages, and the big one:
You cannot revoke it. A token is valid until it expires. Ban a user and their token keeps working. Promote them and their old token still says "customer".
The standard answer is short-lived access tokens plus a refresh token — and a refresh token has to be stored and revocable, which means server state. So the usual JWT setup reintroduces the thing JWTs were supposed to avoid.
Where to store a token — the part people get wrong
localStorage.setItem("token", token); // common, and a real risk
Anything in localStorage is readable by any JavaScript on the page. One
cross-site scripting hole — a compromised npm package, a bad third-party script,
an unescaped review — and every visitor's token can be read and sent elsewhere.
An httpOnly cookie cannot be read by JavaScript at all:
response.cookie("access_token", token, {
httpOnly: true, // JavaScript cannot read it
secure: true, // HTTPS only
sameSite: "lax", // not sent on cross-site requests
maxAge: 15 * 60 * 1000,
path: "/",
});
httpOnly is the important one. XSS can still make requests as the user —
the cookie is sent automatically — but the attacker cannot take the credential
away and use it later. That is a meaningful reduction in damage.
Cookies bring CSRF into scope, since they are sent automatically. sameSite: "lax" handles most of it; module 9 covers the rest.
The rule: store credentials in httpOnly cookies, not in localStorage.
Whether the cookie holds a session id or a JWT is a separate question.
What this course uses
A JWT, in an httpOnly cookie, with a refresh token stored in the database.
The reasoning:
- The cookie means no
localStoragerisk and the browser sends it automatically. - The JWT means the API can verify without a database read on every request.
- The refresh token in the database means revocation is possible.
- The pattern transfers directly to a mobile app later, which a kirana shop plausibly wants.
A plain database session would also be a good choice here, and for a single web client arguably the simpler one. We use JWTs because the mechanics are worth learning and because they appear in most jobs you will take.
The two-token pattern
| Token | Lifetime | Stored | Purpose |
|---|---|---|---|
| Access | 15 minutes | httpOnly cookie | authorises requests |
| Refresh | 30 days | httpOnly cookie and database | gets a new access token |
1. Login issues both
2. Requests carry the access token
3. It expires after 15 minutes
4. Client calls /auth/refresh
5. Server checks the refresh token against the database
6. If valid, issues a new access token
7. Logout deletes the refresh token from the database
Why 15 minutes: a stolen access token is useless quickly, and revocation takes effect within 15 minutes because the refresh will fail.
Why the refresh token is in the database: so it can be deleted. That is the whole revocation story.
Rotating refresh tokens — issuing a new one on each refresh and invalidating the old — lets you detect theft: if an already-used token is presented, something is wrong and you revoke the whole family.
What goes in a token
{
sub: "clx1abc", // the user id
role: "CUSTOMER",
iat: 1727445000,
exp: 1727445900,
}
Small, and nothing private. sub, iat and exp are standard claims.
Do not put a name or email in it. Both are readable by anyone who gets the token, and both go stale — a user changing their email would keep the old one in their token until it expires.
Include the role only if you accept that a role change takes up to 15 minutes to apply. For an e-commerce customer that is fine. For revoking an administrator it is not, and that check should hit the database.
Two things to be clear about
Authentication is who you are. Authorisation is what you may do. A valid token means authenticated; whether you may delete a product is authorisation. They are different checks and produce different status codes — 401 and 403.
Never write your own crypto. Use a maintained library for signing and for password hashing. Both are easy to get subtly wrong in ways that are invisible until exploited.
Check your work
The core problem: HTTP is stateless, so each request must prove who it is from.
Main advantage of sessions: instant revocation, because the state is on the server.
Main disadvantage of JWTs: they cannot be revoked before expiry.
Is a JWT encrypted: no — signed. Anyone holding it can read the payload.
Why localStorage is risky: any JavaScript on the page can read it, so one
XSS hole exposes every visitor's credential.
What httpOnly achieves: JavaScript cannot read the cookie, so an attacker
cannot steal the credential to use elsewhere.
Why the refresh token is in the database: so it can be deleted, which is what makes revocation possible.
Why 15 minutes for an access token: it limits the value of a stolen token and bounds how long a revocation takes to apply.
401 versus 403: not authenticated, versus authenticated but not permitted.
Practice
No code yet — get the decision straight.
- Write down, in your own words, what happens on every request under sessions and under JWTs.
- Take any JWT from jwt.io and decode it. Confirm you can read the payload without any secret.
- For each, say sessions or JWTs and why: a banking app needing instant logout · a public API for third parties · a single web app · a mobile app plus a web app.
- Describe the attack that makes
localStoragerisky, in two sentences. - Explain why
httpOnlydoes not prevent XSS but does reduce its impact. - Explain why a JWT containing
rolemeans a demotion is not immediate. - Decide what this project should use and defend it in a paragraph. If you disagree with the choice above, say what you would do instead.
Next: building registration and login.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship