curl logs in once with a password and gets a signed JWT back, then sends it in an Authorization header — see every HTTP message, the checks a service runs on the token, a second service checking it with only the public key, and what a forged role, alg none, expiry, logout and a revocation list in Redis do.
A bearer token replaces the session's record with a signature.
- curl sends the password once, to
/login, inside TLS. - The server answers with a token: the claims (who, which role, until when, for which services), signed
with the server's private key.
- curl sends the token with every request, as
Authorization: Bearer <token>. - A service checks the signature with the public key, then reads the claims; it looks nothing up.
- It follows
Cookies and Sessions
, where the server kept a record
for every login, and
TLS in Action
, which keeps the token off the
eavesdropper's copy of the wire.
Reading the drawing
- Step plays nine scenes, one message per step: a request, a command to the store, its reply, or a
response.
- Every scene after the first starts with alice's token already in
$TOKEN, so the login is played once. - The client is curl, a program rather than a browser: it keeps the token in a shell variable and
puts it in the header itself.
- The buttons on each card take the story our own way; the scene picker or Restart brings it
back.
- Our own commands on curl's card act on the token: decode it, copy it, edit its role, strip its
signature, delete it.
- The orders service appears beside the server in the scenes that call it, and the revocation
list under the server once logged-out tokens go to Redis.
- The wire shows each message as the two ends read it; TLS encrypts all of it.
- Messages so far, on the right, lists the scene's messages; a click on one goes back to it.
The token: a JWT
The token is a JWT (JSON Web Token): three base64url parts joined by dots, drawn in three colours on
curl's card.
- The header names how the token is signed:
alg (ES256, an elliptic-curve signature) and kid,
the key that made it. - The payload holds the claims:
iss: the issuer, https://shop.example;sub: the subject, alice;aud: the services the token is for;role: a claim of the shop's own;iat and exp: when it was issued and when it expires;jti: the token's own id.
- The signature covers the header and the payload, made with the server's private key.
The payload is encoded, not encrypted: the scene The payload is readable decodes it with
basenc --base64url -d. Anyone holding the token can read every claim, so a payload never carries a secret.
The checks a service runs
Every request with a token goes through the same checks, in this order, and the card of the service
shows each one under Checked the last request.
alg is one the service accepts: ES256, and nothing else.- The signature verifies with the public key of
kid. iss is the issuer the service trusts.aud names this service.exp is still in the future.- Only then are the claims read:
sub says who is asking, role what they may do.
A failed check answers 401 Unauthorized, with WWW-Authenticate: Bearer and an error="invalid_token"
saying which. A good token that does not allow the request answers 403 Forbidden: the service knows who
is asking, and the answer is no.
Two services, one key pair
The server holds the key pair; the orders service holds only the public key.
- The private key signs. It never leaves the server, so only the server can make a token.
- The public key verifies. A service holding it can check any token and make none.
- No round trip. The orders service answers alice's
GET /orders without asking the server or a
store: everything it needs is in the token and its own copy of the public key. - A restart loses nothing. The server keeps no record of the tokens it issued.
This is what sessions could not do: with a session id, every service had to reach the store that held the
records. Here the public key is copied to the orders service by hand; publishing it at a URL for any
service to fetch is the job of an identity provider, a later page.
Forged tokens: an edited role and alg none
The signature is what makes the claims worth reading.
- An edited claim breaks it. The scene A role in the token, and a forged admin changes
role: user
to role: admin and keeps the old signature; the public key no longer verifies it, and /admin answers
401. alg: none asks for no check at all. The JWT standard defines it for unsigned tokens, and some
early libraries read alg from the token and skipped the check when it said none. The service, not
the token, decides which algorithms count: none is refused before the signature is even looked at.
Expiry, logout and revocation
A token carries its own end, exp, under the signature; until then, only a list kept by the services can refuse it.
- Expiry is the one limit every token has. After ten minutes the services refuse it, and curl logs in
again for a new one.
- Logout reaches only the client's own copy. The server answers
204 and has nothing to delete; the
scene Logout and a copy of the token then shows a copy, kept beforehand, still working at both
services until exp. - A revocation list fixes that, and brings the store back.
- Logout writes the token's
jti to Redis, with EX set to the token's remaining lifetime. - Both services ask Redis on every request whether the token was revoked.
- The copy is refused, and every request now waits for the round trip that sessions in Redis needed.
Short lifetimes are the usual answer: a token that lives minutes needs no list, at the price of logging
in again, or of a refresh token, which belongs with OAuth2.
The course
Authentication and Secrets
mints and verifies the
same kind of token with Python in its chapter Bearer tokens and JWT.