runlocally

runlocally engineering notes

Decode JWT

How Decode JWT is built

By Geppetto · · Open Decode JWT →

Decode JWT splits a compact JSON Web Token into its header, payload, and signature, and turns the numeric time claims into readable dates. This post is about what a JWT actually is at the byte level, and the one sentence the tool has to state unconditionally.

Tech used

Base64URL, not Base64

A compact JWT is three dot-separated segments — header.payload.signature — each encoded with Base64URL, a variant of Base64 that swaps +// for -/_ and drops the = padding, precisely so the token survives unescaped inside a URL or an HTTP header. Decoding it is native: atob() after restoring the URL-safe characters to their standard Base64 counterparts and re-padding to a multiple of 4. No JWT library is used — the entire engine is native atob() plus JSON.parse(), run against just the two plaintext segments (header, payload); the signature segment is binary and is shown as raw decoded bytes, not interpreted.

Implementation & operational notes

The tool cannot verify anything, and it says so on every result — not as a footnote. A JWT’s payload is the part everyone wants to read, but reading it proves nothing about whether it’s genuine: verifying a signature requires the issuer’s key, and asking a user to paste a signing secret into a tool that decodes untrusted tokens would recreate the exact problem the tool exists to avoid. So verification is out of scope by design, not by oversight — but leaving that unstated invites a real misuse: treating a decoded payload as trustworthy, which is the shape of the alg: none class of JWT vulnerabilities, where an attacker crafts a token whose claims look legitimate precisely because nothing downstream checks the signature. The result view carries a persistent notice next to the payload — “This tool does not verify the signature. The displayed contents may have been altered.” — worded to be true in every case, not just the suspicious ones.

Standard claims get inline explanations because the three-letter names don’t self-document. exp, iat, and nbf are NumericDate values — seconds since the Unix epoch — and the tool converts each to a local date, flags exp as expired once it’s in the past, and flags nbf as not-yet-valid while it’s in the future. iss, sub, aud, and jti get a one-line label each (issuer, subject, audience, JWT ID) rather than being left as bare keys for someone to look up.

A malformed token gets a specific failure, not a generic one. Decoding can fail at three independent points — wrong segment count, invalid Base64URL, or invalid JSON — and the tool reports which one actually happened instead of collapsing all three into “invalid JWT.” A token with a garbled payload but a well-formed header, for instance, correctly reports the payload as the failure point.

The token itself never reaches the URL. JWTs frequently carry session or authorization data, so the input field is deliberately not reflected into query parameters or hash fragments — pasting a token into the tool and copying the browser’s URL afterward copies nothing back.

Try it / source