Compare server sessions and tokens, and store them safely in cookies or secure storage.
Why it matters
HTTP is stateless: each request arrives on its own, with no memory of the previous one. Yet users expect to log in once and stay logged in. Every API that has accounts needs a way to attach "this is Ada" to later requests. The two main approaches, server sessions and tokens, each have trade-offs, and picking the wrong one, or storing it in the wrong place, causes real security bugs.
Authentication and authorization
Two words that are easy to mix up:
- Authentication (authn): who are you? Proving identity with a password, a Google sign-in, a code from an authenticator app.
- Authorization (authz): what may you do? Ada can edit her own profile but not someone else's; a moderator can hide posts.
This lesson is about carrying the result of authentication across requests. Authorization checks happen on every request after that.
Cookies
A cookie is a small value the server asks the browser to store and send back automatically on every request to the same site:
HTTP/1.1 200 OK
Set-Cookie: sid=8f2c...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800
From then on the browser adds Cookie: sid=8f2c... to each request. The attributes matter:
HttpOnly: JavaScript on the page cannot read the cookie, so a cross-site scripting (XSS) bug cannot steal it.Secure: sent only over HTTPS.SameSite=Lax(orStrict): the browser does not send it on most cross-site requests, which blocks many cross-site request forgery (CSRF) attacks.Max-Age/Expires: how long it lasts. Without one, it is deleted when the browser closes.
Approach 1: server sessions
- The user logs in with a correct password.
- The server creates a random, unguessable session ID, stores
{ sessionId -> userId, expiresAt }in a database or cache, and sends the ID in anHttpOnlycookie. - On each request, the server looks up the ID and loads the user.
- Logging out deletes the session row. The cookie is now worthless.
Strengths: the server can revoke any session instantly ("log out of all devices"), and the cookie holds no data at all. Cost: a lookup per request, and a shared store if you run more than one server.
Approach 2: tokens (JWT)
A JSON Web Token is a string of three base64url parts: header.payload.signature. The payload holds claims:
{ "sub": "user_42", "role": "student", "iat": 1792771200, "exp": 1792774800 }
The server signs it with a secret key. On later requests it verifies the signature and trusts the claims without a database lookup. Important details:
- A JWT is signed, not encrypted. Anyone can decode the payload. Never put secrets or personal data in it.
- Always verify the signature and the expiry with a maintained library, and fix the allowed algorithm. Never accept
alg: none. - A JWT is valid until it expires. Revoking one early needs extra work: short lifetimes plus refresh tokens, a deny list, or a per-user version number stored in the database and checked on each request (which brings back a lookup).
Where the token lives
Tokens can travel in two ways:
HttpOnly cookie | Authorization: Bearer <token> header | |
|---|---|---|
| Sent automatically | Yes, by the browser | No, your code adds it |
| Readable by page scripts | No | Yes, if stored in localStorage |
| Main risk | CSRF (mitigate with SameSite, CSRF tokens) | XSS can steal it |
| Good fit | Browser apps on your own domain | Mobile apps, CLIs, server-to-server |
For browser apps, a session ID or token in an HttpOnly, Secure, SameSite cookie is the safer default. Mobile apps have no cookie jar by default, so they usually send a bearer token and keep it in the platform's secure storage (Keychain on iOS, Keystore on Android). API keys for server-to-server calls are another kind of bearer token.
Worked example: the request flow
POST /login {email, password}
-> check password
-> create session (or sign token)
-> 200 + Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax
GET /me (browser sends Cookie: sid=...)
-> middleware loads the session -> req.user = { id: 42 }
-> 200 { "id": 42, "name": "Ada" }
GET /me (no cookie, or expired)
-> 401
Common mistakes
- Storing tokens in
localStoragein a browser app, where any injected script can read them. - Cookies without
HttpOnly,SecureandSameSite. - Long-lived JWTs with no way to revoke them after a password change or account compromise.
- Personal data or secrets in a JWT payload.
- Checking authentication but not authorization: a logged-in user can still try to read someone else's data.
- Predictable session IDs. Use a cryptographically secure random generator, such as
crypto.randomBytes(32).
Quick recap
- Authentication proves who someone is; authorization decides what they may do.
- Sessions keep state on the server and are easy to revoke; JWTs carry signed claims and are hard to revoke early.
- Browser apps: prefer
HttpOnly,Secure,SameSitecookies. Mobile and server clients: bearer tokens in secure storage. - Whatever you choose, check authorization on every request.
The Do tab quiz asks you to choose the right approach for a few real situations.