Pasting a JWT into an online decoder is a surprisingly bad idea. The payload is just base64url — not encrypted. Whatever is in there (user ID, email, roles, scopes) is visible to whoever runs that server.
I built a JWT Decoder & Inspector that decodes entirely in your browser, with no network requests.
👉 https://jwt-decoder.pages.dev
What It Shows You
Paste any JWT and you get:
-
Header — algorithm (
alg), token type, key ID - Payload — all claims, formatted as pretty-printed JSON
- Signature — the raw base64url value (not verified — more on that below)
Timestamp claims (exp, iat, nbf) are converted from Unix seconds to human-readable local time. The expiration status is shown explicitly:
exp: 2026-07-01 09:00:00 (local) ✅ Valid
or
exp: 2026-06-01 09:00:00 (local) ⚠️ Expired
The base64url Trap
The first implementation detail that trips people up: JWT uses base64url, not standard base64.
The differences:
-
+→- -
/→_ - Trailing
=padding is omitted
Run atob() directly on a base64url string and you'll get an exception or garbage. You need to convert first:
function decodeBase64Url(str) {
let b64 = str.replace(/-/g, "+").replace(/_/g, "/");
while (b64.length % 4) b64 += "=";
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
The second trap: multibyte characters. atob() returns a binary string, not a JavaScript string. If the payload contains non-ASCII content (names in non-Latin scripts, emoji), you'll get mojibake unless you pipe the bytes through TextDecoder. This produces subtle, hard-to-reproduce bugs — works fine for most tokens, breaks on specific ones.
Timestamp Math
JWT timestamps are Unix seconds, not milliseconds. The Date constructor takes milliseconds. Off-by-1000x gives you a date in 1970:
const exp = payload.exp ? new Date(payload.exp * 1000) : null;
const isExpired = exp ? exp.getTime() < Date.now() : false;
Obvious in hindsight, but the bug appears constantly.
Why the Decoder Doesn't Verify Signatures
This was a deliberate design choice: no signature verification.
To verify a JWT signature you need either the secret (HS256) or the public key (RS256/ES256). Adding a "paste your secret here" form would undermine the whole point of the tool. You'd be putting your signing material into a browser text box, which is exactly the kind of thing this tool is trying to avoid.
The tool's job is decoding — reading what's inside the token. Verification belongs in your backend library. The UI makes this explicit with a notice: "Signature is not verified."
Keeping scope narrow makes the tool trustworthy.
No Framework, No Dependencies
JWTs have three parts separated by dots. Splitting on . and decoding each part doesn't need React. A single HTML file with vanilla JS is faster to load, has no CDN dependencies to audit, and works offline from browser cache.
Validated with 31/31 passing tests: base64url edge cases (with and without padding, containing - and _), multibyte payloads, expired vs. valid expiration detection, and malformed JWTs (missing parts, corrupted base64) that display errors instead of throwing exceptions.
Try It
Next time you're debugging auth and need to check what's actually in a token:
👉 JWT Decoder & Inspector — devnestio
All tools: https://devnestio.pages.dev
Works offline. Your token never leaves your browser.
Top comments (0)