JWT Debugger
Decode and verify JSON Web Tokens locally — nothing you paste leaves this tab.
decoded header appears here
decoded payload appears here
the algorithm and key check appear here
Decode, then verify
JWT tooling comes in two layers, and most tools blur the line between them.Decoding is mechanical: a JWT is three base64url segments joined by dots, and the first two are just JSON. Any script can split on the dot and decode them, which is why “decode my JWT” pages are interchangeable — and why none of them are the interesting part. Verifying is cryptographic: recompute the signature over the exactheader.payload bytes with the correct key, and compare. A token that decodes cleanly tells you nothing about whether it is genuine. Anyone can encode{"admin": true}; the signature is what says the issuer actually said it.
Here, decoding happens as you type. Paste a token and the header and payload appear, pretty-printed, with exp, nbf and iat checked against your clock and stated in plain language. Verification is a deliberate second step: paste the key — an HMAC secret, an SPKI public key, or a JWK — and press Verify (⌘⏎Ctrl+Enter). Both steps run entirely in your browser on the Web Crypto API, with no library and no round trip. The page itself is served with connect-src 'none': the browser refuses to let it make any network request at all. That matters here, because a real JWT out of a production system is a credential, and this is one of the few places you should feel safe pasting one.
Decode it without this tool
If you would rather decode in your shell than in a browser tab, these are the snippets that actually work. The first one carries a fix that took longer than it should have.
JWT='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…'
SEG=$(cut -d. -f2 <<< "$JWT")
printf '%s' "$SEG" | tr '_-' '/+' | awk '{print $0 substr("===", 1, (4 - length % 4) % 4)}' | base64 -d | jq .The naive form everyone reaches for first is cut -d. -f2 <<< "$JWT" | base64 -d. It has two bugs, and the second is the interesting one. JWT segments use the URL-safe alphabet (- and _ instead of + and/), which BSD base64 happens to accept. They also omit the trailing= padding, and on macOS and FreeBSD base64 -d does not fail on unpadded input — it exits 0 and silently drops the final partial group. Theawk step above pads the segment to a multiple of four first, which fixes the behaviour everywhere. The FAQ has the details.
import base64, json
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
header, payload, _signature = token.split(".")
data = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
print(data)const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…';
const [, payload] = token.split('.');
// Buffer understands base64url natively on Node ≥ 16 — no manual padding.
console.log(JSON.parse(Buffer.from(payload, 'base64url')));import jwt from 'jsonwebtoken';
try {
// Always pin the algorithms. Many historic JWT bugs came from
// accepting whatever alg the token claimed for itself.
const claims = jwt.verify(token, secret, { algorithms: ['HS256'] });
console.log(claims);
} catch (error) {
console.error(error.message); // "invalid signature", "jwt expired", …
}How a JWT is put together
RFC 7519 defines the format: three segments — header, payload, signature — joined by dots, each base64url-encoded JSON (padding dropped, which is the detail that keeps breaking one-liners). RFC 7515 (JWS) defines how the signature is computed: over the exact bytes header + "." + payload, so any reformatting before verifying breaks it. RFC 7518 (JWA) defines the algorithms this page implements: HS256/384/512 (HMAC — one shared secret, symmetric), RS256/384/512 and PS256/384/512 (RSA), ES256/384/512 (ECDSA), and EdDSA. alg: none is a legal JWA value meaning “unsecured JWT” (RFC 7518 §3.6) — the FAQ explains why libraries that accepted it quietly produced a series of serious vulnerabilities, and why this page refuses to present it as anything but unsigned.
FAQ
Why does my base64 -d one-liner print a truncated payload with no error?
JWT segments are base64url without padding, and the base64 shipped on macOS and FreeBSD does not fail on unpadded input — it exits 0 and silently drops the final partial group. Measured on macOS, not recalled: a payload ending{"sub":"12","exp":1516242622} decodes as{"sub":"12","exp":151624262. Skim the output and you see a plausible timestamp — wrong by a factor of ten. It surfaces as an error only when something downstream like jq chokes on the missing brace, which is easy to misread as a jq problem. The alphabet is not the bug on BSD — base64 -d accepts - and _happily — the padding is. GNU coreutils behaves differently; only the BSD variant was available to measure here. Pad the segment before decoding, as in the shell snippet above, and it works on both. (The base64 page carries the same caveat in full, with the padded and unpadded decode options.)
What is the difference between decoding and verifying?
Decoding expands the base64url and pretty-prints two JSON documents; any three-part string will decode. Verification recomputes the signature over the exactheader.payload bytes with a key, and fails unless it matches. A decoded token may be real, may have been edited, or may carry a random signature — decoding alone proves none of it. This page keeps the two steps separate, and only the second one ever shows a green state.
Why is alg: none dangerous?
RFC 7518 §3.6 defines none for unsecured JWTs — no signature at all. The danger is historical and real: for years, popular libraries accepted whatever alg the header declared, so an attacker could set "alg":"none", send a two-part token with the claims they liked, and have the server treat it as signed. Modern libraries reject it unless explicitly allowed. This page decodes alg: none tokens but never shows them as verified — there is no signature to check, and no UI state that could be misread as one.
Why does ES512 use curve P-521, not P-512?
The number in ES256/384/512 is the SHA digest size in bits, not the curve size. ES512 pairs SHA-512 with the P-521 curve — named for its 521-bit prime, the largest standardised NIST curve. The pairing looks like a typo, and it is the most common thing people get wrong reading the algorithm name, but it is correct per RFC 7518 §3.4. No standardised P-512 curve exists.
Can this page fetch my JWKS by kid?
No, and it is architectural, not missing. This page is served with connect-src 'none': the browser blocks every network request it could make, which is the property that makes it safe to paste a production token into. Fetching a JWKS URL would mean widening that policy to make the request, to save you one paste. Paste the public key directly instead — and if the token carries a kid, it is shown in the decoded header.
Is it safe to paste a production token or signing secret here?
The token in your clipboard is usually a live credential, and the HMAC secret you paste to verify it is the thing that mints more of them — so it is a fair question to ask of any JWT site, including this one. Two answers. The mechanical one: this route is served with connect-src 'none', so the browser itself refuses to let the page make a request, whatever the code tries. The badge under the tool prints the policy actually being sent, and how to read it off the response yourself is a page of its own. The design one: this page has no Copy link button, unlike the other tools here, because a shareable link would mean writing your token into a URL that outlives the tab. Verification runs against the key in the box, and nothing is kept once you close it.