The session from the last chapter has one cost baked into it: the server has to keep state and look you up on every request. The ID in the cookie is meaningless on its own, so the server consults its session store each time to turn that ID back into "you." A token flips the arrangement. Instead of storing anything, the server hands the client a self-contained, signed token, and on later requests it verifies that token's signature to trust the contents. No store, no lookup. That is the stateless alternative we teed up, and the most common shape of it is the JWT.
A token-based system replaces the session lookup with a verification step. At login the server builds a token that already contains the facts about you, signs it with a secret only the server knows, and gives it to the client. The client sends that token back on each request, and the server checks the signature to confirm the token is genuine and unaltered. If the signature holds, the server trusts what is inside, without ever touching a database.

The rest of the chapter unpacks the JWT, the format that carries those self-contained claims:
Strip away the format for a second. A token is just a string the client presents to prove something, and the server can check that proof on its own. The crucial word is self-contained. A session ID proves nothing by itself; its meaning lives in the server's store. A signed token is the opposite. It carries its own meaning ("this is user 4821, role admin"), and it carries a stamp that lets the server confirm the meaning was not forged or changed.
So the server's job changes from remember and look up to . That is the whole shift, and everything else in this chapter is the mechanics of how one string can be both readable and tamper-evident at the same time.
A JWT (JSON Web Token, pronounced "jot") is the most common token format. It is one long string made of three parts joined by dots:
textxxxxx.yyyyy.zzzzz
Each part is a piece of base64-encoded text, and each has a distinct job.

The first part is the header. It says how the token is built: which signing algorithm was used (for example HS256) and that the type is JWT. Decoded, it is small:
json{ "alg": "HS256", "typ": "JWT" }
The second part is the payload, and it holds the claims: the actual statements about you. A claim is just a key/value fact the token asserts, like your user ID, your role, and when the token expires.
json{ "userId": 4821, "role": "admin", "exp": 1719843200 }
The third part is the signature, and it is the part that makes the whole thing trustworthy. The server takes the header and payload, and runs them through a signing function together with a secret key that only the server holds. The result is the signature.
Here is why that matters. The header and payload are out in the open, so anyone can read them and anyone could try to change them. But to change the payload and have the server still accept it, you would need to produce a new matching signature, and that requires the secret. Without the secret you cannot. So if someone flips "role": "admin" into the payload of a token they were handed, the signature no longer matches the contents, and the server's verification fails. The token is rejected.
That is the core trick: anyone can read a JWT, but only the holder of the secret can produce one the server will accept. Reading is open; forging is not.
This is the single most important thing to get right about JWTs, and it is the part beginners most often get backwards.
A JWT is signed, not encrypted. Signing protects integrity: it proves the contents were not changed. It does nothing to hide them. The payload is just base64-encoded JSON, and base64 is encoding, not encryption. There is no secret involved in reading it. Paste any JWT into a decoder, or even decode the middle chunk by hand, and the claims read straight back as plain text.
Let's make that concrete. Here is a token and what its parts decode to.

The header and payload come back as ordinary JSON. Nothing about the token kept them secret. The signature is the only piece you cannot reproduce, and that is by design: its job is to catch tampering, not to hide the payload.
The rule that falls straight out of this is short and absolute:
Never put a secret in a JWT payload. No passwords, no API keys, no private personal data. Assume the payload is public, because to anyone holding the token, it is. Put identifying claims in there (a user ID, a role, an expiry) and keep the secrets on the server.
If you remember one sentence from this chapter, make it that one. Plenty of real security incidents trace back to someone treating a JWT like an encrypted envelope when it is really a sealed-but-transparent one: you can see through it, you just cannot alter it.
The stateless design is clean, but it is not free. Three problems show up the moment you run tokens in production, and being honest about them is the only way to use JWTs well.
Expiry. Because the server does not track the token, it needs a way to stop trusting one eventually. That is the exp claim: a timestamp baked into the payload that says when the token is no longer valid. The server checks it on every verification and rejects anything past its expiry. The guidance is to keep access tokens short-lived, often minutes, so that a leaked token is only useful for a small window.
Refresh tokens. Short-lived tokens create an obvious annoyance: if your token dies after fifteen minutes, do you log in again every fifteen minutes? No. The usual fix is a second, longer-lived refresh token. The short access token is what rides on each request; when it expires, the client quietly trades the refresh token for a fresh access token, no password needed. You get short-lived access tokens for safety and long-lived sessions for comfort, by splitting the two jobs across two tokens.
Revocation. This is the genuinely hard one, and it is the main trade-off against sessions. With a server-side session, logging someone out is trivial: delete the session record, and the next lookup fails. With a stateless token there is nothing to delete. The token is self-contained and already in the client's hands, so until its exp passes, the server will keep accepting it. If an access token is stolen, or a user is banned, or a password is changed, you cannot cleanly invalidate an already-issued token the way you would delete a session.
Teams work around this, but every workaround chips at the "stateless" promise. They keep tokens short so the bad window is small. They keep a server-side denylist of revoked tokens that verification checks against, which reintroduces exactly the lookup JWTs were supposed to avoid. Or they revoke the refresh token so no new access tokens can be minted, and just wait out the short-lived one. There is no free lunch here, and pretending otherwise is how JWT systems get into trouble.
Neither approach wins outright. They trade the same property in opposite directions: sessions keep state on the server and pay a lookup; JWTs push state to the client and skip the lookup. Everything else follows from that one choice.
| Sessions | JWTs | |
|---|---|---|
| Where state lives | Server (session store) | Inside the token, on the client |
| Per-request cost | A lookup in the store | Verify a signature, no lookup |
| Logging out / revoking | Easy: delete the record | Hard: nothing to delete until expiry |
| Scaling across servers | Needs a shared store (Redis) | Any server with the secret can verify |
| Natural fit | Classic web apps, one site | APIs and services that span many backends |
Reach for sessions when easy revocation matters and you control the whole server side: a classic logged-in web app, where instant logout, account bans, and "sign out everywhere" are features users expect. The server already holds the state, so killing access is one delete away.
Reach for JWTs when you want statelessness and reach: an API consumed by many clients, or a fleet of independent services that all need to check "who is this?" without sharing a session store. Any service that holds the secret can verify a token on its own, which is exactly what you want across a distributed system. You accept the harder revocation story in exchange for not needing a central store on the hot path.
Both are correct, widely used designs. The skill is matching the trade-off to your system, not picking a favorite.
One mechanical question is still open: the token is built and verified, but how does the client actually attach it to a request? It travels in a request header, and that header has a specific, standard shape.
The next chapter, "Authorization Headers and API Keys," shows how a credential actually rides on a request: the Authorization: Bearer <token> header that carries a JWT, plus API keys and the other schemes you will meet.