Lesson 24 of 32 intermediate 6 min read

Before this:Authentication & sessions

Cookies, tokens & JWTs

Key takeaways The session identifier has to ride on every request — the three common vehicles are cookies, bearer tokens, and JWTs. A cookie is stored by the browser and sent automatically; harden it with the HttpOnly, Secure, and SameSite flags. A bearer token is sent deliberately in an Authorization header, common for APIs. A JWT is a signed, not encrypted token that carries its own claims so the server can trust it without a lookup — powerful, but hard to revoke before it expires. Which you pick trades convenience against control.

The previous lesson ended with a session identified by a token the browser sends back on every request. This lesson is about that token: what form it takes, how it travels, and how to keep it from being stolen. Get this wrong and a stolen token means a stolen account — so the security flags here matter as much as the mechanics.

Cookies: the browser’s memory

A cookie is a small named value the server asks the browser to store, using a Set-Cookie response header. From then on the browser automatically includes it on every request to that site — you write no code to send it. That automatic behaviour is what makes cookies the classic home for a session ID.

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600

Three flags on that header do the security heavy lifting:

  • HttpOnly — JavaScript can’t read the cookie. This blunts cross-site scripting from stealing the session, because a script that runs on your page still can’t see the value.
  • Secure — the cookie is only sent over HTTPS, so it never crosses the network in the clear. (See TLS & HTTPS.)
  • SameSite — controls whether the cookie rides along on requests coming from other sites. Lax or Strict is a primary defence against CSRF, which abuses the browser’s automatic cookie-sending.

A session cookie holds an opaque ID; the real state lives on the server. That makes logout trivial — the server forgets the session and the ID is worthless.

Tokens: identity you send on purpose

The other model hands the client a token — a string it stores itself and sends explicitly, usually as a bearer token in an HTTP header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Nothing is automatic here: your front-end code attaches the header on each request. That extra work buys flexibility — the same API can serve a browser app, a mobile app, and other servers, none of which share the browser’s cookie machinery. Because tokens aren’t sent automatically, they sidestep CSRF; the cost is that you must store them somewhere safe, and browser storage readable by JavaScript is exposed to XSS.

JWTs: a token that carries its own claims

A JSON Web Token (JWT) is a specific, popular token format. It packs a small JSON payload — the claims, such as the user ID and an expiry time — and signs it. The three dot-separated parts are a header, the payload, and a signature:

header.payload.signature   ->   eyJ...  .  eyJ...  .  SflKx...

The signature is the point. The server signs the token with a secret at login; on each later request it re-checks the signature. If it matches, the claims are trustworthy and the server needs no database lookup to know who you are — the identity travels inside the token. That statelessness is the JWT’s whole appeal, especially across many servers.

Two things trip people up, both worth memorising:

  • A JWT is signed, not encrypted. The payload is readable by anyone holding the token. Never put a secret in it, and always send it over HTTPS.
  • A self-contained JWT is hard to revoke. Because the server stores nothing, a leaked token is valid until it expires — deleting it server-side isn’t possible. The fix is short lifetimes plus a refresh token (a longer-lived credential used to mint new short access tokens), or a server-side revocation list that trades away some of the statelessness.

Choosing between them

There’s no universal winner — match the tool to the client:

  Session cookie Bearer token / JWT
Sent Automatically by the browser Deliberately, in a header
State Server-side (opaque ID) Often self-contained (JWT)
Revoke / logout Easy — drop the session Hard for stateless JWTs
Main risk CSRF (mitigate with SameSite) Theft via XSS if JS-readable
Good fit Classic server-rendered web apps APIs, mobile, cross-service

A common, sound default for a browser app is session cookies with HttpOnly, Secure, and SameSite set. Reach for tokens when non-browser clients or many independent services are in the picture. Whatever you choose, the token is the keys to the account — protect it as such.

Quick check: is it safe to store a user's secret data inside a JWT payload?

Recap

  • The session token has to travel on every request; the three vehicles are cookies, bearer tokens, and JWTs.
  • A cookie is stored and sent automatically by the browser; harden it with HttpOnly (no JS access), Secure (HTTPS only), and SameSite (limits cross-site sending).
  • A bearer token is sent deliberately in an Authorization header, which suits APIs and non-browser clients and avoids CSRF.
  • A JWT carries signed claims so the server can trust it without a lookup — but it’s readable (not encrypted) and hard to revoke before it expires.
  • Match the tool to the client: cookies for classic web apps, tokens for APIs and cross-service; either way, guard the token like the account it unlocks.

Next up: WebSockets & real-time updates.

Frequently asked questions

What's the difference between a cookie and a token?

A cookie is a small named value the browser stores and automatically attaches to matching requests — you don’t write code to send it. A token (like a JWT) is a string your code stores and sends deliberately, usually in an Authorization header. Cookies are convenient for browser apps; tokens are common for APIs and mobile clients. They’re not mutually exclusive — a token can even be delivered inside a cookie.

Are JWTs encrypted?

No — a standard JWT is signed, not encrypted. Anyone holding it can read its contents (the payload is just base64-encoded JSON), but the signature lets the server detect tampering. So never put secrets in a JWT payload, and always send it over HTTPS so it can’t be read or stolen in transit.

Why can't I just log the user out by deleting their JWT?

Because a self-contained JWT is valid until it expires, no matter what the server does — the server keeps no record of it to delete. That’s the tradeoff for being stateless. Real systems keep JWT lifetimes short and pair them with a refresh token or a revocation list when instant logout matters.