Privacy & how we build
JWT in your tab vs jwt.io
Decoding a JWT is three base64url segments and a JSON.parse. Anyone with the token can read the payload. Verification is a crypto check against a secret or a public key, and it belongs in your app, not in a website you googled. The JWT decoder keeps the bytes in this page and links the source file. jwt.io does the same algebra in a textarea. The difference is the trust boundary when third-party scripts enter, and the culture of pasting that textarea into Slack. Same math. Different dump.
The tool page already covers the microscope: header and payload decoded, signature shown and not checked, alg: none is someone else’s bug, expired exp still renders, do not paste a customer token, JWE is not this tool. This note is the part that page left as a stub: jwt.io is not “local” in the way people mean, and screenshots are how tokens leave a tab that never POSTed them.
jwt.io is a third-party origin
Auth0’s debugger is famous because the decode runs in the browser. That sentence is true and incomplete. The HTML still loads from jwt.io. That origin ships whatever JavaScript the operator ships this week: analytics, marketing, error reporting, A/B, a framework you did not review. Those scripts share the page with the textarea. Same-origin script can read the token you pasted. You are trusting Auth0’s current deploy, their vendors, and the next engineer who adds a tag. That can be a reasonable trust for a documentation token. It is a silly trust for a production customer JWT that carries an email, a tenant id, and a role.
I am not alleging jwt.io exfiltrates tokens today. I am saying you cannot audit the next script tag from a Slack thread. View source on my decoder is a file in this repo. View source on jwt.io is a product page. If your threat model is “I do not want this string on someone else’s server,” a third-party origin is already the wrong room, even when the debugger function itself never calls fetch. Browser extensions can read both pages. Mine discloses that. So should your runbook.
The classic jwt.io example token is fine to paste anywhere. It is a fixture. Production tokens are not fixtures. Encoding is not encryption. HS256 in the header does not mean the payload is secret. It means someone with the secret could have signed it — and anyone with the token can still read it.
Screenshot culture is the actual exfil
Support Slack fills with JWTs because they are one line and they look like a receipt. Someone pastes the header into a thread. Someone else says “drop it in jwt.io.” A third person posts a screenshot of the decoded payload because the JSON is easier to read than the base64. The token has now been: in Slack’s message store, in Slack search, in phone notifications, in any compliance export, on jwt.io’s origin in a DOM that third-party scripts can see, and in the screenshot file, which will be copied into Jira by Friday. Rotating the token after that is the adult move. Pretending the decoder “didn’t upload it” is a category error. The humans uploaded it. The tools just made the JSON pretty.
Zoom is the same failure with a live audience. Shared screen, jwt.io, a customer email in sub, intern in the gallery view, recording on. Redact before you share, or do not share. If you must inspect a customer token, do it locally, on a machine that is not presenting, and do not photograph the result. If it hit a ticket system, rotate. Logging Authorization headers in application logs is the industrial version of this habit. Breach reports keep finding JWTs because they are easy to copy. Easy to copy is not a feature of a credential.
Worked example: the documentation token, two origins
Paste the classic jwt.io example — the one in their docs, the one every tutorial clones — into jwt.io and into this decoder. Header says HS256. Payload has sub, name, admin. Both pages show the same JSON. On jwt.io you also loaded whatever scripts that origin currently serves. On this page you loaded my jwt-decoder.js, plus whatever you already allowed in the cookie banner for ads and analytics, which still do not receive the textarea from the decoder script. Read what “runs in the browser” does and does not mean if that distinction is the part you came for. You have learned the shape of a JWT. You have not learned whether a server would accept this token. Neither page verified the signature. If you then screenshot the payload into a public Discord “jwt help” channel, you taught a room full of strangers a fixture. Fine. Do that with a customer token and you taught them a session.
Worked example: the Slack screenshot with an email in it
A support engineer pastes a live access token. Payload decodes to an email, a tenant, exp in ninety minutes. They screenshot jwt.io so the JSON wraps. Six people emoji the thread. Ninety minutes later the token is expired; Slack still has the screenshot; jwt.io’s tab may still be open on a laptop that sleeps instead of locking. Expiry is a claim. It is not a shredder. Anyone who copied the string in those ninety minutes could have used it. Anyone with the screenshot still has PII. The local decoder would have been the less-wrong place to look, and still the wrong place to photograph. Rotate. Redact. Put a dummy token in the ticket. If your runbook says “decode on jwt.io,” change the runbook.
Numbers, because people ask: a compact JWT is often 800–2000 characters. That is small enough to sit in a Slack mobile notification preview. It is large enough to hold an email and a list of scopes. Length is not safety. Base64url is not safety. A green “decoded” badge is not verification.
What this note adds that the decoder does not
The tool is a microscope with a trust rail. This note is why jwt.io’s client-side story is not the same as “the token stayed with me,” and why the screenshot is the leak even when no fetch fired. Decode is not verify. Production customer tokens do not belong in jwt.io, in this textarea, in Slack, or in Zoom. Use the JWT generator for fixtures with disposable secrets. Use your test suite to verify signatures. If you pasted a real token into any of the above, rotate it. I cannot unsend a screenshot.