What's Actually Inside a JWT (and Why Decoding Isn't the Same as Verifying)
A JWT looks intimidating the first time you see one - a long string of three chunks separated by dots, like
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.abc123. But there's no
encryption or magic hiding inside it. It's three pieces of plain text, base64url-encoded, glued together with
periods. Once you understand what those three pieces are and, more importantly, what looking at them does and
doesn't tell you, a JWT stops being mysterious and starts being just another data format with a specific
security property you need to respect.
The Three-Part Structure
A JWT is header.payload.signature - three segments, each
base64url-encoded, joined by two dots. Base64url is a URL-safe variant of ordinary base64: it swaps
+ for - and
/ for _, and drops
the trailing = padding entirely, because both of the substituted
characters and the padding character have special meaning in URLs and cookies. If you ever try to decode a JWT
segment with a generic base64 tool and get garbage or an error, this substitution (plus the missing padding) is
almost always why.
Header
A small JSON object declaring alg
(the signing algorithm, e.g. HS256) and
typ (almost always JWT).
Payload
The actual claims - things like sub
(subject), iat (issued-at), exp
(expiration), aud (audience), plus whatever custom fields the
issuer added.
Signature
A MAC or digital signature computed over the first two segments, used to prove the token wasn't tampered with after it was issued.
Decoding Requires Nothing. Verifying Requires the Key.
This is the point that actually matters, and it's worth stating without hedging: the header and payload are encoded, not encrypted. Base64url is not a secret - it's a reversible text encoding, the same as regular base64, just with different characters substituted. Anyone holding the token string can split it on the dots and decode the first two parts with nothing more than a text editor and five minutes, no key, no password, no permission required. If a JWT contains a user's email, role, or internal ID in a claim, that data is as readable as if it were sitting in a plain JSON file - encoding it into a JWT does not hide it.
Verifying is a completely different operation, and it's the one that actually matters for security. Verifying a JWT means recomputing the signature over the header and payload using the issuer's secret (for HMAC algorithms like HS256) or checking it against the issuer's public key (for RSA/ECDSA algorithms like RS256 or ES256), and confirming it matches the signature attached to the token. That step is the only thing that proves the token was actually issued by whoever holds the key, and that its contents haven't been altered since. Decoding tells you what a token claims. Verifying tells you whether you should believe it.
The failure mode this leads to is depressingly simple to construct: take any JSON object, base64url-encode it as a "header," take any other JSON object and encode it as a "payload," then append literally any string after the second dot. The result decodes without error - it has three segments, valid JSON in the first two, and something in the third - but nothing about that structure proves it was issued by a legitimate authority. A system that decodes a JWT's payload and trusts the claims inside it without checking the signature against the correct key is one crafted token away from an attacker impersonating any user they like.
Seeing This in Practice
You can watch this distinction play out directly with the tools on this site. The JWT Decoder splits a token on its dots, base64url-decodes the header and payload, and shows you the resulting JSON - and its "Validate" button checks only that the structure is well-formed (three parts, valid base64url, valid JSON in each), not that the signature checks out. That's not a limitation unique to this tool; it's true of any decoder running in a browser, because real signature verification needs the issuer's secret or public key, which by design never ships to a client that isn't supposed to hold it.
For building test tokens, the JWT Encoder lets you construct a header and payload and stitch them into a properly-shaped three-part token - genuinely useful for seeing how a specific combination of claims looks once encoded, or for feeding a deliberately malformed token into your own code to test error handling. Worth knowing going in: its "signature" when you supply a secret is not a standards-compliant HMAC-SHA256 computation, so a token it produces won't pass verification against a real backend using the same secret - it's a tool for learning the shape of a JWT and generating test payloads, not for minting tokens a production system should trust.
The Takeaway for Anyone Building Auth
If your application logic ever branches on a claim pulled out of a JWT - checking a role, trusting a user ID, granting access to a resource - that check is only as trustworthy as the signature verification that happened before it. Decode a token to debug it, inspect it, or understand what a service is sending you. But treat every claim inside an unverified token as untrusted input, exactly the way you'd treat a value a user typed into a form field, because structurally that's all it is until the signature says otherwise.
