Understanding JSON Web Tokens (JWT)
What lives inside a token, and what actually makes it trustworthy.
A JSON Web Token (JWT) is a compact, URL-safe way to carry a set of claims between two parties. You’ve almost certainly seen one: a long string of letters, numbers, hyphens and underscores with two dots in it. That string is really three separate pieces glued together, and once you can read those pieces apart, JWTs stop feeling like magic. The single most important thing to understand up front is that a JWT is encoded, not encrypted — anyone holding the token can read what’s inside it.
The three parts: header.payload.signature
A JWT is three base64url-encoded segments separated by dots, in the form
header.payload.signature. The first two segments are just JSON that has been
base64url-encoded so it survives being placed in a URL or an HTTP header. The third is a
cryptographic signature over the first two. You can paste any token into our
JWT decoder to split it apart and read the header and payload
instantly — it runs entirely in your browser, so the token never leaves your machine.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.eyJzdWIiOiIxMjM0IiwiZXhwIjoxNzAwMDB9 ← payload
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ← signature The header: which algorithm signed this?
The header is a small JSON object that describes how the token was produced. Two fields
dominate: typ (the media type, almost always "JWT") and
alg (the signing algorithm). The algorithm tells a verifier how to check the
signature. You’ll commonly see one of these families:
| Algorithm | Family | Signed with | Verified with |
|---|---|---|---|
HS256 | HMAC + SHA-256 | A shared secret | The same shared secret |
RS256 | RSA + SHA-256 | A private key | The matching public key |
ES256 | ECDSA + SHA-256 | An EC private key | The matching public key |
The distinction matters. With HS256 the same secret both creates and checks the
signature, so every party that can verify can also forge. With RS256 or
ES256 a private key signs and a freely shareable public key verifies — ideal when
many independent services need to validate tokens issued by one authority.
The payload: claims about the subject
The payload carries the claims — statements about an entity (typically a user) plus metadata about the token itself. The spec defines a handful of registered claims with reserved three-letter names. They’re optional but widely used, and good tooling knows how to interpret them:
iss · issuer
Who created and signed the token, e.g. your auth server.
sub · subject
Who or what the token is about — usually a user id.
aud · audience
The recipient(s) the token is intended for.
exp · expiry
Unix time after which the token must be rejected.
iat · issued at
Unix time the token was created — useful for token age.
nbf · not before
Unix time before which the token is not yet valid.
Alongside these you can include any custom claims you like — a role, a tenant id, a display name. But because the payload is only base64url-encoded, treat it as public. If you’re curious, you can confirm this yourself: copy a token’s middle segment into our Base64 encoder & decoder and the raw JSON pops straight out, no key required. The rule follows directly: never put passwords, secrets or sensitive personal data in a JWT payload.
The signature: what proves the token is genuine
The signature is the part that makes a JWT trustworthy. It is computed over the encoded header
and payload joined by a dot — conceptually
sign(base64url(header) + "." + base64url(payload), key) — using the algorithm
named in the header. Because the signature depends on the exact bytes of the header and
payload, changing a single character in either one invalidates it. An attacker can decode and
read a token freely, but they can’t alter the claims and produce a matching signature without
the secret (for HMAC) or the private key (for RSA/EC).
Verifying a token means three things, in order: recompute the signature over
the received header and payload and confirm it matches the signature segment; confirm the
alg is one you actually expect (don’t blindly trust the header — reject
"none" and algorithm-confusion attacks); and check the time-based claims, above
all that exp is still in the future. Only if all of these pass should the claims
be believed. Skipping the expiry check is a classic, dangerous mistake.
Where JWTs shine: stateless authentication
The most common use is stateless auth. After you log in, the server signs a
JWT describing who you are and sends it back. Your client then attaches it to each request,
usually as Authorization: Bearer <token>. The server verifies the
signature and reads the claims to know who you are — without a database lookup or a session
stored on the server. That statelessness is what makes JWTs scale so well across many
instances and microservices.
The trade-off is revocation: because nothing is stored server-side, a valid token stays valid
until it expires. That’s why production systems keep token lifetimes short (minutes, via
exp) and pair them with longer-lived refresh tokens. Keep access tokens brief,
always verify the signature and expiry, and never trust the payload as private storage.
Related tools: JWT decoder · Base64 encoder & decoder