Toolshed

A growing library of browser tools and deep technical guides for IT professionals.

← All guides

What's actually inside a JWT, and why decoding one isn't the same as verifying it

4 min read

A JWT (JSON Web Token, defined in RFC 7519) shows up constantly in modern auth — session tokens, API credentials, single sign-on. The eyJhbGci... string looks encrypted. It isn't. Understanding what's actually inside one, and the difference between reading it and trusting it, matters for anyone debugging auth issues or reviewing how a system handles them.

Three parts, separated by dots

A JWT is three Base64URL-encoded segments joined by periods: header.payload.signature. Base64URL is the same idea as ordinary Base64, swapped to a URL-safe alphabet (- and _ instead of + and /, padding usually stripped) so the token can sit safely in a URL or an HTTP header without extra escaping.

The header is a small JSON object naming the signing algorithm and token type:

{"alg": "HS256", "typ": "JWT"}

The payload is the actual content — a JSON object of "claims." Some claim names are standardized by RFC 7519 (though none are strictly required):

exp, iat, and nbf are all Unix timestamps — seconds since 1970-01-01 UTC — which is why a raw payload showing "exp": 1735689600 is hard to sanity-check without converting it to an actual date first.

The signature is what makes the first two parts trustworthy — a cryptographic signature over the header and payload, computed with a secret (for symmetric algorithms like HS256) or a private key (for asymmetric ones like RS256).

Base64 is not encryption

This is the part that catches people off guard: anyone can decode the header and payload of any JWT with nothing more than a text editor and a Base64 decoder. There's no secret required to read a JWT — only to verify one, or to forge a valid one. Don't put anything in a JWT payload you wouldn't be comfortable showing to whoever holds the token, even if it's never displayed anywhere in your UI.

Decoding vs. verifying — the distinction that matters

Decoding takes the header and payload apart and shows you the JSON. It requires no key of any kind, and it tells you absolutely nothing about whether the token is legitimate — a token with a payload of {"sub": "admin", "role": "superuser"} decodes and reads identically whether it was legitimately issued or hand-crafted by an attacker in five seconds on jwt.io.

Verifying recomputes the signature using the expected key and checks it matches the signature segment on the token. Only a system that actually performs this check — using a key it controls, not one read out of the token — can trust the claims inside.

This distinction is also the root of a real, historically-exploited vulnerability class: libraries that let the token itself specify which algorithm to verify with (reading alg from the header rather than having the server pin it) opened the door to attacks like switching alg to none (some early library implementations honored this and skipped verification entirely) or swapping an asymmetric algorithm for a symmetric one and signing with the public key, which the server may have been treating as a shared secret. Current guidance across JWT libraries is consistent: the verifying side should specify the expected algorithm explicitly, never trust the alg field from the token being checked.

What a decoder tool can and can't tell you

A pure decoder — reading header and payload, no key involved — can tell you what a token claims. It cannot tell you whether those claims are true. If you're debugging why a token seems to carry the wrong permissions or an unexpected expiration, a decoder gets you to the actual claim values fast. If the question is "should this system have accepted this token," that's a verification question, and answering it needs the actual signing key and the code path that's supposed to check it.

Sources: RFC 7519 (JSON Web Token); RFC 4648 §5 (Base64URL encoding); RFC 7518 (JSON Web Algorithms, covering HS256/RS256 and related signing algorithms).