protocol · 02 / 05JWS · ES256

JSON Web Tokens

A token anyone can read but no one can forge.

What it does

A JSON Web Token is two ideas from earlier on this site, snapped together. Take some claims — “this user is Ada, and she's an admin” — write them as JSON, encode them so they survive being stuffed into an HTTP header, and then sign them so nobody can tamper with what you said.

The result lets a server trust a request without a database lookup. It hands you a signed token at login; you present it on every request; the server checks the signature and believes the claims inside, because it knows only it could have signed them.

Three parts, joined by dots

A JWT is literally three Base64URL strings with dots between them:

base64url(header) . base64url(payload) . base64url(signature)

  • Header — a tiny JSON object naming the algorithm, like {"alg":"ES256","typ":"JWT"}.
  • Payload — the claims: who you are, what you can do, when the token expires.
  • Signature — a signature computed over the first two parts. Change either one and this stops matching.

Read it, then try to forge it

The token below is issued and signed in your browser. First notice you can read every claim — no key needed. Then edit the role claim to admin and watch what the server does:

JWS · ES256 — issued and signed by the server

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDExIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzAwMDAwMDAwfQ.

header payload signature

decoded — no key required, it's only Base64
header
{
  "alg": "ES256",
  "typ": "JWT"
}
payload
{
  "sub": "1011",
  "name": "Ada Lovelace",
  "role": "user",
  "iat": 1700000000
}

Token rejected

You changed a claim, so the payload bytes no longer match the signature. Only the server's private key could mint a signature for role: user— and you don't have it.

Encoded is not encrypted

This trips up nearly everyone: the payload is not secret. It's Base64, not encryption — the same lesson from the encoding page. Anyone holding the token can read every claim in it. So never put a password, a card number, or anything private in a JWT payload. The signature stops tampering, not reading. If you need the contents hidden too, encrypt them.

Why the signature is the whole point

Because the claims are readable and self-contained, the obvious attack is to just edit them — change role: user to role: admin and send it back. The signature is what makes that fail. It covers the exact bytes of the header and payload, so any edit invalidates it, and only the holder of the signing key can produce a fresh valid one. This is the signatures lesson doing its job, applied to a token.

Two flavors are common. HS256 signs with a shared secret (an HMAC) — simple, but everyone who can verify can also forge, so it only works inside one trust boundary. ES256 and RS256 sign with a private key and verify with the public one, so a third party can check a token without being able to mint it. The demo above uses ES256.

Claims have an expiry — and that's load-bearing

Because nothing is looked up server-side, a stolen token is valid until it expires. That's why payloads carry an exp timestamp and real systems keep token lifetimes short, pairing a brief access token with a longer-lived refresh token. A signature proves a claim is authentic; it can't un-issue one.

What it's for

  • Stateless sessions — APIs verify a signed token instead of hitting a session store on every request.
  • Single sign-on — OpenID Connect ships identity as a signed JWT, so one login works across many services.
  • Service-to-service auth — signed tokens let backends prove who they are to each other without sharing passwords.