By Abu Sufyan · Lead Systems Architect, WebToolkit Pro
Every week, thousands of developers do something they know they shouldn't.
They open a new browser tab, navigate to some third-party JWT decoder, and paste a live production token directly into it.
That token probably contains a user ID. Maybe a role claim. Maybe an internal service identifier that maps to something sensitive in your infrastructure. And it just left your browser.
I know, because I used to do it too.
The Incident That Made Me Stop
In late 2024, I was debugging an auth issue on a client's Node.js API — a financial services platform. Standard stuff: the access token wasn't refreshing correctly and I needed to inspect the payload quickly to verify the exp claim.
I instinctively opened a well-known JWT decoder site and pasted the token.
Then I stopped.
I was working in a production environment. That token belonged to a real user — a client of theirs. It contained a sub (subject) claim with a UUID that mapped directly to their user database. It contained an iat and exp. And most critically, it contained a roles claim: ["admin", "finance_read"].
I had just sent a production admin token to a third-party server I knew nothing about.
The site claimed it was "client-side only." But I couldn't verify that. There was no open-source repo I could inspect. No audit trail. No CSP headers I could check. Just a text box and a promise.
That afternoon, I filed an internal incident report and started building a replacement.
What Makes a JWT Decoder Dangerous
Before I get to what I built, it's worth understanding exactly why this matters technically — not just philosophically.
A JWT has three parts: header, payload, and signature. The first two are just Base64url-encoded JSON. Decoding them requires zero network traffic. It's pure string manipulation you can do in two lines of JavaScript:
const [header, payload] = token.split('.');
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
That's it. There is no cryptographic reason a JWT decoder needs to contact any server whatsoever.
And yet, when you paste a token into most online decoders, your token travels over the network to a server. That server may or may not be logging requests. It may be behind a CDN that caches POST bodies. It may be owned by a company whose threat model you have no visibility into.
The attack surface is real:
- Session hijacking if the token is still valid and the attacker can replay it against your API
- Claim exposure — user IDs, email addresses, role names, tenant identifiers
-
Infrastructure mapping — the
iss(issuer) claim often reveals internal service URLs or identity provider endpoints -
Algorithm fingerprinting — the
algheader tells an attacker exactly what signing algorithm you're using, useful for crafting exploit attempts
The Architecture: Zero-Knowledge Client-Side Decoding
What I built — the Offline JWT Decoder at WebToolkit Pro — operates on a simple constraint: nothing ever leaves the browser tab.
Here's the actual execution model:
User pastes token → Browser JS splits on '.'
→ atob() decodes header + payload
→ JSON.parse() reconstructs the object
→ Rendered directly into DOM
No fetch(). No XMLHttpRequest(). No WebSocket. No Service Worker relay.
To verify this yourself: open DevTools → Network tab → paste a token → watch. Zero outbound requests. The network panel stays empty.
The implementation uses the Web Crypto API for signature verification — which also runs entirely in the browser sandbox. There are no API keys, no backend, no telemetry. The Content Security Policy on the page blocks outbound requests at the browser level, so even if a malicious script injection somehow occurred, it couldn't exfiltrate the token.
Beyond Decoding: The Signature Verification Problem
Most online JWT decoders only decode. They don't verify.
Decoding tells you what's inside the token. Verification tells you whether to trust it.
The difference matters enormously in debugging scenarios. If you're diagnosing an auth failure, you need to know:
- Is the token malformed? (decoding fails)
- Is it expired? (check
expvs current timestamp) - Is the signature valid against the expected public key? (actual cryptographic verification)
Point 3 is where most tools fall apart. They skip it entirely, leaving you with no way to confirm whether the token was legitimately issued or tampered with.
The JWT Debugger & Inspector I built handles all three. You can paste your JWKS (JSON Web Key Set) endpoint URL or paste the raw public key directly, and the tool performs RS256/ES256 signature verification locally using window.crypto.subtle.verify() — the same Web Crypto primitive your browser uses for HTTPS.
// What's happening under the hood
const key = await crypto.subtle.importKey(
'jwk',
jwkPublicKey,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['verify']
);
const isValid = await crypto.subtle.verify(
'RSASSA-PKCS1-v1_5',
key,
signature,
data
);
No server. No SDK. Pure browser cryptography.
The alg: none Attack — Why Your Decoder Needs to Flag This
While building this, I added one feature that I think every JWT tool should have: automatic detection of the alg: none exploit vector.
Here's the attack. Some JWT libraries — particularly older versions — accept tokens where the algorithm is set to none and the signature is empty. An attacker who knows this can forge a token with any payload they want:
// Header
{
"alg": "none",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"roles": ["admin"],
"iat": 1516239022
}
// Signature: empty string
This gets concatenated as base64(header).base64(payload). — note the trailing dot with no signature.
The decoder flags this immediately with a warning: ⚠ Algorithm "none" detected. This token carries no cryptographic integrity guarantee. Reject in production.
It's a small thing, but it's the kind of context-aware output you only get from a tool built by someone who actually works with these tokens in production.
What I Learned Building Privacy-First Tools
This JWT decoder is one of 150+ tools I've built at WebToolkit Pro. Every single one follows the same constraint: your data never leaves your browser.
Password generator. Hash generator. Base64 encoder. Regex tester. JSON formatter. AES encryption tool. All of them execute entirely client-side.
Building this way taught me a few things:
WebAssembly changed what's possible. Operations that used to require a server — like Argon2 password hashing, which is deliberately memory-intensive — can now run in the browser via WASM. The Argon2 Hasher generates production-grade password hashes locally in under a second.
Web Workers are underused. For tools that process large inputs (the JSON formatter handles payloads up to 50MB), moving the parsing off the main thread via Web Workers keeps the UI responsive. No spinners, no freezes.
The hardest part isn't the crypto — it's the UX. Security tools have a reputation for being hostile to use. I spent more time on the output formatting and error messaging than on the underlying algorithms. A tool that correctly identifies an exp claim mismatch but displays it as a raw timestamp integer has failed its user.
Try It Yourself
If you're debugging JWTs today, open DevTools first. Network tab, record. Then go to wtkpro.site/tools/jwt-decoder and paste your token.
Watch the network panel stay silent.
That silence is the feature.
Links
- Offline JWT Decoder — decode without sending tokens to any server
- JWT Debugger & Inspector — full signature verification with JWKS support
- JWT Signing Tool — generate signed tokens locally for testing
- Password Entropy Tester — offline entropy calculation and crack time estimates
- Full tool directory: wtkpro.site/tools
Abu Sufyan is the founder of WebToolkit Pro and a systems architect specializing in V8 performance, security tooling, and zero-knowledge web architectures. Find him on GitHub and Dev.to.
Tags: security jwt webdev javascript privacy
Top comments (0)