Security FIELD NOTES · 5 MIN READ
Inside a JWT: Decoding the Token
What the three dot-separated parts of a JSON Web Token contain: base64url header and claims, the HMAC signature, and why decoding is not verifying.
A JSON Web Token looks like line noise — three clumps of letters, digits, and the occasional dash, separated by dots. In reality it is one of the most readable structures on the web: two of its three parts are just base64url-encoded JSON that anyone can decode, and the third is a signature that only the server's key can produce. Understanding the parts turns a mysterious string into a plain (but signed) document.
Three parts, two dots
Every JWT follows the same layout:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.cMErWtEf7DxCXJl8C9q0L7ttkm-Ex54UWHsOCMGbtUc
The header and payload are Base64 with a URL-safe twist: the characters + and / are replaced by - and _ (so a token can travel inside a URL without escaping), and the trailing = padding is stripped — which is why token parts so often end mid-multiple-of-4 (see How Base64 Encoding Works for the padding rules). Decoding the first part above gives {"alg":"HS256","typ":"JWT"}. A tell you can spot at a glance: any base64 of text starting with { begins with the characters eyJ, which is why real-world tokens almost always start with eyJ.
The header and the payload
The header names the token type and — importantly — the signing algorithm (alg). The payload carries the claims: statements about who the token belongs to and what it may do. Some claim names are registered by the JWT standard:
{"sub":"1234567890", subject — whom the token is about
"name":"Jane Doe", a public claim, application-defined
"iat":1516239022} issued-at, as Unix seconds
The registered set covers the usual bookkeeping: iss (issuer), sub (subject), aud (audience), exp (expiry), nbf (not before), iat (issued at), and jti (a unique token id). Timestamps are Unix seconds — 1516239022 is January 2018 — and the Unix Time article explains that counting method. Anything else is fair game: an application can add role, email, or whatever its checks need.
The signature
The third part is what makes a JWT more than a JSON postcard. For the HS256 algorithm named in this header, the signature is an HMAC over the first two parts, using a key known only to the issuing server:
signature = HMAC-SHA256(key, base64url(header) + "." + base64url(payload))
Altering one character of the payload — extending an expiry, promoting a role — invalidates the signature, and only holders of the key can compute the corrected one. The HMAC article walks through that construction in detail. Other algorithms put a different mechanism in the same slot: RS256 and ES256 sign with the issuer's private key and are verified with a public key, but the three-part structure is identical.
Decoding is not verifying
This is the most important line in the article: anyone can decode a JWT; only the key can validate one. Decoding is pure base64 — reversible without any secret, which is why decoded payloads are safe to inspect anywhere. Verification means recomputing the signature from the received header and payload with the server's key and comparing it to the third part; that step needs the key and is why it belongs on the server, on every request. A decoder that shows you the payload is showing you claims as written, not claims as proven — an expired or forged token decodes exactly as smoothly as a valid one. That is why the JWT Decoder is decode-only by design: it lays out the header, payload, expiry and issued-at timestamps so you can inspect them, while leaving the cryptographic question — does the signature actually check out? — to the server that holds the key.
What a token can and cannot hide
Because the payload is encoded rather than encrypted, it is readable by anyone who obtains the token — the browser's devtools, a proxy log, the URL it rode in on. The right mental model is a laminated ID badge: anyone can read what is printed on it, but only the issuer can produce a badge with a valid seal. So claims must never carry data meant to stay private; sensitive payloads belong in opaque references (an id the server can look up) rather than in the token itself. Two more behaviors worth knowing: expiry (exp) is not self-enforcing — the token does not stop working at that moment, the verifier rejects it once the timestamp passes. And history has punished verifiers that trusted the header's alg field blindly: early libraries accepted tokens that declared alg: none (no signature at all), letting forged tokens through. Modern libraries pin the algorithm they expect, and the lesson generalizes: the header is attacker-controlled input, not a promise.
Try it yourself
Paste the full three-part token from the top of this article into the JWT Decoder and watch it split cleanly into header, payload, and signature, with the iat and exp timestamps rendered as readable dates. Then edit one character of the payload — change Jane to John — and see that it still decodes perfectly: decoding checks structure, not authenticity. To complete the picture, decode the header and payload parts yourself with the Base64 Encoder (add the = padding back if your decoder complains), reproduce the signature by HMAC-ing the two dot-joined parts in the HMAC Generator, and read HMAC: How Messages Prove They Weren't Tampered With for the machinery behind it.