JWT Decoding Guide: Header, Payload, Signature, and Safe Base64URL Inspection
Decode a JWT by hand: split the three dot-separated parts, reverse the Base64URL encoding, read the claims, and inspect tokens safely without trusting them.
JWT Decoding Guide: Header, Payload, Signature, and Safe Base64URL Inspection
A JSON Web Token looks like line noise until you know the trick: it is three chunks of Base64URL text glued together with dots. Once you can take one apart, a whole class of authentication bugs becomes debuggable in seconds — wrong audience, expired token, missing role claim, mismatched algorithm. This guide walks through the anatomy of a real token, shows exactly how Base64URL differs from the Base64 you already know, and covers the safety rules that matter when you paste a production token anywhere.
The three parts: header, payload, signature
Here is a real HS256 token I generated for this article (the secret is toolora-demo-secret, so feel free to verify it yourself):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQ4MjEiLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzUxNDE0NDAwLCJleHAiOjE3NTE0MTgwMDB9.u59YJdPIAYfbW1ldCglfGx_Ra-T5qdfKkFpccFgJ3lI
Split it on the two dots and you get three independent pieces:
Part 1, the header — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 decodes to:
{"alg":"HS256","typ":"JWT"}
The header tells you which signing algorithm the issuer claims to have used. That word "claims" is doing heavy lifting: the header is attacker-controlled input, and verification libraries that blindly honor alg (especially the infamous "alg":"none") have caused real CVEs. When you decode, read the header first — if your API expects RS256 and the token says HS256, you have already found your bug.
Part 2, the payload — eyJzdWIiOiJ1c2VyXzQ4MjEiLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzUxNDE0NDAwLCJleHAiOjE3NTE0MTgwMDB9 decodes to:
{"sub":"user_4821","role":"editor","iat":1751414400,"exp":1751418000}
These are the claims. iat and exp are Unix timestamps — this token was issued at 1751414400 and expires exactly 3600 seconds later, a one-hour lifetime. If a request fails with a 401 and the decoded exp is in the past, you are done debugging: the token is simply expired.
Part 3, the signature — u59YJdPIAYfbW1ldCglfGx_Ra-T5qdfKkFpccFgJ3lI is not JSON. It is 32 raw bytes of HMAC-SHA256 output, computed over header + "." + payload with the secret key, then Base64URL-encoded. You cannot "decode" it into anything readable, and that is the point: it only becomes meaningful when a verifier recomputes it and compares.
Base64URL is not quite Base64 — three exact differences
The encoding used by JWTs is Base64URL (RFC 4648 §5), and it differs from standard Base64 in three specific ways:
+becomes-(minus)/becomes_(underscore)- The
=padding at the end is stripped
Look at the signature above: it contains both _ and -. In standard Base64, that same value would be u59YJdPIAYfbW1ldCglfGx/Ra+T5qdfKkFpccFgJ3lI= — a /, a +, and a trailing =. Those three characters are exactly the ones that break URLs and query strings (+ turns into a space, / splits paths, = delimits parameters), which is why the JWT spec swapped them out. Tokens ride in URLs, headers, and cookies all day; the alphabet had to survive that.
This is also why pasting a JWT segment into a plain Base64 decoder sometimes fails with an "invalid character" error. If you hit that, run the segment through a converter that speaks both alphabets — Toolora's Base64URL encoder/decoder accepts padded or unpadded input and shows you the standard-Base64 equivalent side by side. For ordinary non-JWT strings, the regular Base64 encoder covers the classic alphabet.
One number worth keeping in your head: Base64 in any variant expands data by roughly 33%, because every 3 input bytes become 4 output characters (per RFC 4648). Our 69-byte payload JSON became a 92-character segment — 92/69 ≈ 1.33, right on the predicted ratio. If a "small" token is pushing 4 KB, the decoded claims are around 3 KB, and that usually means someone stuffed an entire user object into the payload.
Decoding by hand, and when not to
I generated the token above with Node's crypto module and then deliberately broke it while writing this section: I flipped one character in the payload segment and re-decoded it. The JSON came out corrupted mid-string, but plenty of decoders will still happily render the readable portion — and the signature check, which I was skipping, is the only thing that would have told me the token was tampered with. That half-hour convinced me to keep two habits: always look at the header's alg before trusting anything else, and never treat a successful decode as a successful verification.
Decoding by hand is genuinely useful for learning. In Node it is three lines:
const [h, p] = token.split(".");
console.log(JSON.parse(Buffer.from(h, "base64url").toString()));
console.log(JSON.parse(Buffer.from(p, "base64url").toString()));
But for day-to-day debugging, a purpose-built viewer is faster: Toolora's JWT decoder splits the token, reverses the Base64URL on the header and payload, pretty-prints the claims, and converts iat/exp timestamps into human-readable dates — all in your browser, with nothing sent to a server. Its FAQ says it plainly and it bears repeating here: decoding is not verifying. The tool shows you what the token asserts; only your backend, holding the key, can confirm the assertion.
Safe inspection: rules for handling real tokens
A JWT payload is only encoded, not encrypted. Anyone who obtains the token reads everything in it. That leads to a short list of rules I now follow without exception:
- Never paste production tokens into random online decoders. A token pasted into a server-side tool may be logged, and a live access token in a log file is a credential leak. Use a client-side decoder (check the network tab: zero requests should fire when you paste) or decode locally in your terminal.
- Treat every decoded claim as unverified input. The payload says
"role":"editor"— so what? Until the signature is checked against the issuer's key, that is a plain-text assertion anyone could have typed. - Watch the clock skew.
expcomparisons happen in seconds. If a token seems to expire "early," compare the decodedexpagainst the actual server time, not your laptop's. - Don't put secrets in payloads you issue. Email addresses, internal IDs, and feature flags are common in claims; passwords, API keys, and PII beyond what the consumer needs should never be. Assume every token you mint will eventually be decoded by someone you didn't expect.
- Expired demo tokens only in docs. The token in this article expired one hour after issuance, on purpose. If you write internal documentation with example tokens, mint them with an
expin the past so a copy-pasted example can never authenticate.
A 30-second debugging checklist
Next time an API rejects a token, decode it and run down this list: Is alg what the server expects? Is exp in the future and iat in the past? Do iss and aud match the environment you are calling (staging tokens against production APIs are a classic)? Is the claim your code reads — role, scope, sub — actually present and spelled the way the middleware expects? In my experience, four out of five "mysterious 401" bugs fall to one of those four questions, and none of them require the signing key. That is the real payoff of understanding JWT structure: the readable two-thirds of the token answers most questions, as long as you remember the unreadable third is the only part you can trust.
Made by Toolora · Updated 2026-07-02