Go look at any online JWT tool. Somewhere on the page there's a line telling you not to paste production tokens.
Nobody listens. The whole reason you opened the tool is that a token is broken, and the broken one is a real one. So you paste it, and now a live session token is sitting in some stranger's server logs.
I got annoyed enough about this to build the alternative. It's called TokenBench.
The rule I gave myself
Nothing you type ever leaves the page.
Not "we don't store it." Not "we delete it after 30 seconds." It never goes anywhere in the first place, because there's nothing on the other end to receive it.
Everything runs through WebCrypto in the browser. Decoding, signature verification, key parsing, token generation — all client-side. You can open devtools, sit on the Network tab, paste a token, and watch nothing happen.
Then close the tab. Still nothing.
That last part matters more than it sounds like it should, and I'll come back to it.
Two verdicts, not one
The thing that actually pushed me to build this wasn't privacy, it was a bad afternoon.
A token was failing. The tool I was using showed me a red banner. Red banner means bad signature, right? So I went and checked the signing key. Rotated it. Checked the deploy config. Checked whether staging and prod were sharing secrets.
The signature was fine. The token was expired.
Those are two completely different failures with two completely different fixes, and collapsing them into one red banner cost me an hour. So TokenBench always shows both rows separately:
- Signature: does it match the key you supplied
-
Time: is it inside its
exp/nbfwindow
A correctly signed, expired token is the single most common real-world state, and it should not look like a forgery.
Algorithm confusion, and refusing to help
If you hand TokenBench an HS256 token and a public key, it won't verify it. It stops and explains why.
Quick version: RS256 signs with a private key, verifies with a public one. The public key is public. If your server reads the algorithm out of the token's own header and dispatches on it, an attacker flips the header to HS256, signs the token using your public key's bytes as the HMAC secret, and your code cheerfully verifies a forgery using information anyone can download.
// don't
const header = JSON.parse(base64url.decode(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] });
// do
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
The algorithm is your decision, not the token's. A tool that quietly performs the confused operation is teaching the bug, so this one won't do it.
There's a generator too, which can deliberately emit tampered signatures and alg: none tokens - so you can point them at your own server and confirm it actually rejects them.
The part I got wrong
Original build had analytics. Not third-party - my own, first-party, and deliberately careful: counters only, hardcoded allowlist of event names, nothing token-derived could physically get into the payload. It held everything in memory and sent one small beacon when you closed the page, specifically so that pasting a token fired nothing.
I was pleased with that design. It was defensible.
Then I did a final pass before launch, sat on the Network tab, and watched the beacon go out.
The page says nothing is sent. Something was sent. Doesn't matter that it was harmless. The whole pitch is "don't trust me, check" - and the first person to actually check would find a claim that needed a footnote.
So it's gone. No analytics, no beacon, no cookies. I get page-view counts from my host's server logs like it's 2004, and the claim is now just true with no asterisk.
Weirdly this is the part I'd defend hardest. Privacy claims that need explaining aren't privacy claims, they're marketing.
What's in it
Four tools: decoder, validator, generator, secret key generator. All the JOSE algorithms - HS/RS/PS/ES 256/384/512 and EdDSA. Keys accepted as raw secrets, PEM (SPKI and PKCS#1), X.509 certs, JWK or JWKS. Bearer prefixes, line breaks, and URL-encoding get stripped for you because tokens never arrive clean.
There's security linting throughout: weak secrets, alg: none, missing exp, well-known tutorial secrets.
Works offline once loaded. Free, no signup.
tokenbench.dev — source at github.com/kalisada/tokenbench if you want to verify any of the above rather than take my word for it.
Happy to hear where it falls short.
Top comments (14)
The algorithm-confusion refusal is the detail that stands out — I've seen that exact
jwt.verify(token, publicKey, { algorithms: [header.alg] })pattern in real codebases, almost always copy-pasted from a Stack Overflow answer nobody read past the first green checkmark. It's a clean target for a static-analysis rule too: flag anyalgorithmsarray whose value traces back to the token payload instead of a literal in the code. The beacon anecdote is honestly the better lesson though — "trust me, nothing is sent" is a claim, and the only way to verify a claim is to watch the wire yourself, same as I'd tell anyone reviewing AI-generated code not to trust the docstring over the diff.The static-analysis idea is good - "algorithms array traces back to the token" is exactly the taint pattern. And yes, the beacon lesson generalizes: the claim is never the artifact, the wire is.
The two-verdict split is the right call, but there's a third row hiding inside your "Time" row that bites people harder than expiry:
nbffailures caused by clock skew, not by a genuinely-not-yet-valid token. A token minted on a server whose clock is 40 seconds ahead of the validator reads as "not yet valid" for those 40 seconds, and it looks identical to a config bug. Most production JWT libraries default to a leeway/clock-tolerance window precisely because of this, and a lot of people don't know it exists until they trip over it. If your Time row just says pass/fail against the rawexp/nbf, consider showing the actual delta — "expired 8s ago" or "nbf in the future by 31s" — because "how far off" is exactly the signal that tells you skew from a real staleness problem.One thing I'd verify rather than assume: your alg-confusion refusal is correct for the HS256-with-public-key case, but the nastier variant is when someone genuinely runs a mixed HS/RS setup and legitimately passes a shared secret. Make sure the refusal is keyed on "you handed me a public key for an HMAC alg," not just "the header says HS256," or you'll false-positive on the people who actually have a valid reason to be there.
Update: shipped. Turns out the deltas were already in the verdicts ("EXPIRED 3 minutes ago"), but your skew point was the real gap - deltas under ~90 seconds now explicitly say they're likely clock skew and that most verifiers allow leeway. Live now, with tests. Thank you again for making the tool better.
No problem, glad I could help. Take care!!
Both points are well taken. Showing the delta ("expired 8s ago" / "nbf 31s in the future") is a genuinely better design than pass/fail and I'm going to add it; you're right that the magnitude is what distinguishes skew from staleness.
On the refusal: it's keyed on the key material, not the header - a shared secret with an HS256 token verifies normally; the refusal only fires when you hand asymmetric public key material to an HMAC alg. Mixed HS/RS setups work.
This is awesome! How does it actually not matter if you paste real tokens? Is it
Thanks! It's all WebCrypto in the browser - decoding, signature verification, key parsing, generation. There's no backend to send anything to.
The way to check rather than take my word for it: open devtools, select the Network tab, paste a token. Nothing fires. Then close the tab - still no network traffic. No analytics, no cookies, no beacons anywhere on the site. Source is public if you want to read it: github.com/kalisada/tokenbench
The rule of never sending the token to a server is exactly the kind of constraint that changes whether a debugging tool is safe to use under pressure. I also like the split between signature validity and time validity, because a lot of JWT tooling collapses those into a vague green or red result that does not help during incident triage. In practice the next useful step is making the unsafe cases impossible by design, not just documented in a warning banner. Nice example of turning a security footgun into a product constraint.
"Impossible by design, not documented in a warning" is the phrasing I wish I'd used in the article. Thanks.
Interesting take on JWT debugging—removing the risk of token exposure is a solid security win. In my work with secure GPU environments, I've seen similar patterns where isolating sensitive operations in air-gapped or confidential computing contexts can eliminate side-channel risks. It's reassuring to see this principle applied to a common developer tool.
Thanks - same principle, different scale: the cheapest side channel to eliminate is the one you never create.
Browser-local inspection is the right trust boundary for this kind of tool. The best security UX is not a warning that users will eventually ignore, but a design where the risky data never leaves.
That's exactly it. A warning is a request to be careful; a design where the data can't leave doesn't need the user to be careful at all.