How to decode a JWT without a tool
A JWT's header and payload are just base64url-encoded JSON — no library, no website, and no secret key required to read them. Here is exactly how, and where the technique stops working.
Step 1: split on the dots
A JWT (technically a JWS in compact serialization) is three base64url
segments joined by periods: header.payload.signature. Splitting
on . gives you the three pieces to work with. If you don't get
exactly three, what you're looking at either isn't a JWT or has been
truncated somewhere — a real token always has exactly two dots.
Step 2: undo the URL-safe substitution and re-add padding
Standard base64 uses + and /, both of which are
meaningful inside a URL. Since JWTs routinely travel in URLs and headers,
they use the base64url variant instead: swap every
- back to + and every _ back to
/. Base64url also drops the trailing = padding
that standard base64 requires, so add it back: pad the segment with
= characters until its length is a multiple of 4.
segment.length % 4 === 2 -> add "=="
segment.length % 4 === 3 -> add "="
segment.length % 4 === 0 -> no padding needed
Step 3: base64-decode and read the JSON
Once you have standard, padded base64, any base64 decoder recovers the original bytes, which are UTF-8 text encoding a JSON object. In a shell:
echo '<padded, +/-substituted header segment>' | base64 -d
In a browser console, the built-in atob() does the same thing
for a payload without any embedded Unicode outside the ASCII range:
JSON.parse(atob(paddedSegment)). Do this for both the header
and the payload segments; you now have two readable JSON objects with no
library, no website, and no key of any kind.
Why the signature segment is different
The third segment is not encoded JSON — it's the raw output bytes of a cryptographic signature (an HMAC digest, or an RSA/ECDSA signature) computed over the first two segments. Running it through the same decode steps above gives you back binary data with no JSON structure to recover, because there was never a JSON object encoded into it in the first place. Reading the header and payload tells you what the token claims; only verifying the signature against the correct key tells you whether those claims can be trusted, and that step needs a real JWT library and the key — there is no by-hand shortcut for it, and there shouldn't be one.
What a clean decode does and doesn't tell you
Decoding recovers exactly what was written into the token, nothing more.
Anyone can hand-craft a header and payload that decode perfectly cleanly
— a well-formed, readable JSON payload is not evidence the token is
genuine, current, or intended for the service reading it. That's the gap
this checker is built for: once you can read a token's
claims, it also flags the specific structural patterns worth a second look
— alg: none, an embedded key header, a suspiciously long
lifetime, a sensitive-looking field name — the same read-through
either way, just with the known failure shapes already checked for you.
Common questions
Do I need the secret key to decode a JWT?
No. The header and payload are base64url-encoded, not encrypted -- decoding them back to readable JSON needs no key at all. The secret or private key is only involved in verifying the signature segment (proving the token was not tampered with), which is a completely separate step from reading the claims.
Why can't I decode the third segment the same way?
The third segment is the raw signature bytes -- a cryptographic output, not encoded JSON. Running it through a base64url decoder gives you back binary data with no readable structure, because there was never a JSON object encoded into it to recover. It exists to be verified against the header and payload using a key, not to be read.
Why does base64url use - and _ instead of + and /?
Standard base64 uses + and /, both of which have special meaning in a URL (query separators and path segments). JWTs are routinely passed in URLs and HTTP headers, so RFC 7519 specifies the URL-safe base64url variant, which substitutes - for + and _ for /, and drops the trailing = padding characters entirely. A generic base64 decoder that doesn't know this will fail or produce garbage on a real JWT segment until you swap those characters back and re-add padding.
Does a clean decode mean the token is valid?
No. Decoding only recovers the claims that were written into the token -- it proves nothing about whether those claims are genuine. A token with a completely invalid or missing signature decodes exactly as cleanly as a correctly signed one; anyone can hand-craft a header and payload that decode perfectly. Only verifying the signature against the correct key, using a real JWT library, proves the token wasn't tampered with.