How to Decode a JWT Safely in the Browser: Local Decoding, Leaked Tokens and Storage
Decoding a JWT is harmless. The header and payload are just Base64url-encoded JSON, and reading them requires no key. Where you decode it is not harmless. A JWT is a bearer credential: whoever holds it can use it until it expires. Pasting a production token into a website that sends it to a server, attaching it to a ticket or leaving it in a screenshot hands over access.
Why the token itself is the risk
An access token in an Authorization: Bearer … header is accepted by the API from anyone who presents it. It is not tied to your browser or IP address, unless the system uses sender-constrained tokens such as DPoP or mutual TLS, which most do not. Until its exp passes, a copied token works exactly like a logged-in session.
Refresh tokens are worse. They can mint new access tokens for days or weeks. ID tokens from OpenID Connect contain personal data such as email and name, which is sensitive even after they expire.
So the rule is simple: decoding is fine; sending the token anywhere you do not control is not.
Decode locally, in the browser console
You do not need a website to decode a JWT. The browser console on any page will do it, and the token never leaves your machine.
function decodeJwt(token) {
const part = (s) => {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/')
.padEnd(Math.ceil(s.length / 4) * 4, '=');
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes)); // UTF-8 safe
};
const [header, payload] = token.split('.');
return { header: part(header), payload: part(payload) };
}
const { payload } = decodeJwt(token);
new Date(payload.exp * 1000).toISOString(); // exp is in seconds
Two details make naive snippets fail. JWTs use Base64url without padding, so - and _ must be mapped back and = restored before atob. And atob returns one character per byte, so names with accents or non-Latin scripts come out garbled unless the bytes are decoded as UTF-8, which is what the TextDecoder step does.
Online decoders that run entirely in the browser are equally safe, but you cannot easily verify that a page does not send what you paste. Check the Network tab while decoding, or use an offline tool. For production tokens, the console is the zero-trust option.
Decoding is not validating
A decoded payload tells you what the token claims, not whether it is genuine. Anyone can create a token with "role": "admin"; only signature verification with the issuer's key shows it is real. Front-end code may decode a token to show the user's name or schedule a refresh before exp, but every authorisation decision must happen on the server, after verification.
The reverse also holds. Do not send a token to a validation service just to check it. Verification needs only the issuer's public key (for RS256 or ES256), which you can use locally with a library such as jose.
Keeping tokens out of logs, tickets and screenshots
Most leaks are accidental. Tokens end up in shared HAR files, in bug reports with a copied curl command, in CI logs that print request headers, in analytics or error tracking that captures URLs with ?token=, and in screenshots of DevTools.
Redact before sharing. Keep the header and payload if they are needed for debugging, and remove the signature, which makes the token unusable while leaving the claims readable. Replace it with a marker such as .REDACTED. Chrome DevTools can export a sanitised HAR without cookies and authorisation headers. Use it, or edit the file before attaching it.
# strip the signature before pasting a token into a ticket
printf '%s' "$TOKEN" | cut -d. -f1,2 | sed 's/$/.REDACTED/'
# scrub Authorization headers from a saved HAR file
jq '(.log.entries[].request.headers[] | select(.name|ascii_downcase=="authorization").value) = "REDACTED"' in.har > out.har
Configure your own systems to do the same. Logging frameworks and error trackers such as Sentry support scrubbing rules for Authorization, Cookie and parameters named token, and they should be on by default. Never put tokens in URLs. Query strings are recorded in server logs, browser history and Referer headers.
If a token has leaked
Access tokens usually cannot be revoked individually. Check exp: if it is minutes away, the exposure is short. Refresh tokens can and should be revoked through the provider's revocation endpoint (RFC 7009) or admin console, together with the user's sessions.
If a signing key leaked, as opposed to a single token, every token signed with it must be distrusted. Rotate the key, remove the old one from the JWKS, and accept that users will need to sign in again.
Keeping access tokens short-lived, 5 to 15 minutes, is what makes a leaked token a minor incident instead of a major one.
Where the browser should keep tokens
Anything JavaScript can read, JavaScript injected through an XSS flaw can also read. That includes localStorage, sessionStorage and variables in memory. localStorage also persists after the tab is closed.
For first-party web apps, the most robust pattern keeps tokens out of JavaScript entirely. The server, or a backend-for-frontend, holds the tokens and gives the browser a session cookie marked HttpOnly, Secure and SameSite=Lax or Strict. The IETF draft "OAuth 2.0 for Browser-Based Applications" recommends this pattern. When a single-page app must hold tokens, keep them in memory, keep them short-lived, and obtain refresh tokens with rotation enabled.
Sources and further reading
RFC 6750 defines bearer token usage and warns against tokens in URLs. RFC 7519 and RFC 7515 define JWT and its Base64url encoding. RFC 7009 defines token revocation, and RFC 9449 defines DPoP sender-constrained tokens. The IETF OAuth working group's draft "OAuth 2.0 for Browser-Based Applications" discusses token storage. MDN documents atob() and TextDecoder.
Frequently asked questions
Is it safe to paste a JWT into an online decoder?
Only if the decoder runs entirely in your browser and the token is not a production credential you care about. A JWT works for anyone who holds it until it expires, so decode sensitive tokens locally, for example in the browser console.
How do I decode a JWT in JavaScript?
Split on `.`, convert each part from Base64url to Base64 (`-`→`+`, `_`→`/`, add `=` padding), decode with `atob`, turn the result into UTF-8 with `TextDecoder`, and `JSON.parse` it. Libraries such as `jose` provide `decodeJwt()` for the payload.
Does decoding a JWT verify it?
No. Decoding only reads the claims, and anyone can create a token with any claims. Verification checks the signature with the issuer's key and must happen on the server before the claims are trusted.
What should I do if I leaked a JWT?
Check its `exp`: short-lived access tokens expire soon. Revoke any refresh token and the user's sessions through the identity provider. If a signing key leaked, rotate it and remove it from the JWKS.
Should I store JWTs in localStorage?
It is convenient but readable by any script, including injected XSS code. For web apps, prefer HttpOnly, Secure, SameSite cookies managed by a backend; if a SPA must hold tokens, keep them in memory and short-lived.
How can I share a JWT for debugging without leaking it?
Remove the signature, the third part, before sharing. The header and payload stay readable for debugging, but the token can no longer be used.