A JSON Web Token looks like an opaque wall of characters, but most of it is plain text anyone can read. The dangerous confusion is between reading a token and trusting it.

A JSON Web Token (JWT) is the string that sits behind most modern login sessions and API authentication. You have seen it: a long run of letters, numbers, dashes and underscores with two dots in it, arriving in an Authorization: Bearer ... header. It looks encrypted. It looks like something only the server could possibly read. Both of those impressions are wrong, and the gap between what a JWT looks like and what it actually is causes a specific, recurring class of security bug.

The single most important idea in this article: decoding a JWT is not the same as verifying it. Anyone can decode one. Only the holder of the right key can verify one. Confusing the two is how tokens get trusted that should never have been trusted.

The three parts

A JWT is three sections joined by dots: header.payload.signature. Each of the first two sections is a JSON object that has been base64url-encoded. The third is the cryptographic signature over the first two. Split on the dots and you have the whole structure:

The header and payload are encoded, not encrypted. That distinction is the whole game.

base64url is encoding, not encryption

Each part is encoded with base64url — a URL-safe variant of Base64 that uses - and _ instead of + and /, and drops the trailing = padding so the token survives inside URLs and headers untouched. Encoding is a reversible transformation with no key involved. Anyone who receives the token can split it on the dots, base64url-decode the middle section, and read every claim in plain text.

This has a blunt practical consequence: whatever you put in a JWT payload is public to anyone who holds the token. The browser can read it. Any proxy that sees the request can read it. A copy pasted into a decoder can read it. The signature stops people from changing the payload without detection; it does nothing to stop them from reading it. If you would not print a value on a postcard, it does not belong in a JWT payload.

Standard claims

RFC 7519 defines a set of registered claim names — three-letter keys with agreed meanings so that different systems interpret them the same way. The common ones:

The timestamps are seconds since the Unix epoch, not human dates — a decoder that converts exp to a readable time saves you the arithmetic. Everything beyond these registered names is a custom (private) claim: roles, tenant IDs, feature flags. All of it, registered or custom, is readable by anyone with the token.

How signature verification works

The signature is what lets a server accept a token it did not itself store. Rather than looking the token up in a database, the server recomputes the signature over the header and payload it received and checks that it matches the signature attached. If a single character of the payload was altered, the recomputed signature will not match and the token is rejected. This is why the mechanism is trustworthy despite the payload being readable — readable is fine, tamper-evident is the point.

There are two broad families of algorithm, and the difference matters:

Aspect HS256 (symmetric) RS256 (asymmetric)
KeysOne shared secretPrivate key + public key pair
Signs withThe shared secretThe private key
Verifies withThe same shared secretThe public key
Best whenOne party both signs and verifiesMany parties verify, only one signs

HS256 uses one secret for both signing and verifying (an HMAC over SHA-256). It is simple and fast, but everyone who can verify can also mint tokens — so the secret cannot be shared with parties you only want to read tokens. RS256 uses a key pair: the issuer signs with a private key it guards closely, and anyone can verify with the freely distributable public key. Because the public key cannot create signatures, you can hand it to any number of downstream services and none of them can forge a token. That property is why RS256 dominates federated setups, single sign-on, and public APIs.

The common mistakes

When to decode versus when to verify

Decoding is a legitimate, everyday operation — for reading, debugging, and inspection. You decode when you want to see what is inside a token: checking why a login failed, confirming which claims your identity provider issues, or reading a non-security field for display. Decoding requires no key and asserts no trust.

You verify whenever the answer will drive a decision: granting access, returning a user's data, allowing an action. Verification requires the correct key and must include the expiry and audience checks, not just the signature. As a rule of thumb: decode freely to look, but verify before you act.

Tool walkthrough

Toolhub's JWT decoder splits a token on its dots and base64url-decodes the header and payload so you can read the claims as formatted JSON, converting the exp, iat and nbf timestamps into readable times. It runs entirely in the browser, so a token you paste is never transmitted anywhere — which matters, because a live token is a credential. Note what the decoder deliberately does not do: it shows you the header's stated algorithm but it does not tell you the token is genuine, because verification needs the signing key, which lives on your server and should stay there.

If you want to see the raw mechanics underneath, the base64 encoder lets you take a single section of a JWT and base64url-decode it by hand — proof, in one step, that the payload was never secret to begin with. Encoding a small JSON object the same way shows exactly how the header and payload sections are built.

Where to read further

Treat a JWT as a signed postcard: the message is there for anyone to read, and the value is entirely in the seal that proves who wrote it. Read them freely to understand what is going on, but never let a decoded claim make a decision on your behalf. Decoding shows you what a token says; only verification tells you whether to believe it.

← All articles