Skip to main content

JWT Decoder

Decode JWT header and payload entirely in your browser. Your token is never uploaded, logged or sent anywhere. Handles base64url, UTF-8 claims and JWE detection.

Your data never leaves the browserUpdated: July 2026

This tool only decodes; it does NOT verify the signature. A decoded token is not a trusted token, and verification requires the signing secret or public key, server side.

Paste a token to decode it.

Key Takeaways

  • Decoding is not verification: a decoded token is not a trusted token, and no claim should drive a decision before the signature is checked.
  • JWT segments use base64url (the - and _ alphabet, no padding), so a bare atob() call either throws or mis-decodes most tokens.
  • exp, iat and nbf are seconds since epoch; new Date(exp) lands in 1970, and the expiry verdict depends on the viewer device clock.
  • This tool runs entirely in your browser and deliberately has no "secret" field; never paste a signing key into a web page.

Decoding is not verification

A JWT has three parts, and the first two are not encrypted, only encoded. Anyone holding the token can read them. What this decoder (or any other) shows you is not "this token is authentic" but "this is what the token says". That distinction is the difference between a working authentication flow and a vulnerability.

Verifying the signature requires the issuer secret or public key, and that check belongs on the server where the key already lives. This is why the tool intentionally has no "secret" input: teaching people to paste signing keys into arbitrary web pages creates a far bigger problem than it solves. Once a key leaves your infrastructure, every token it could ever sign becomes forgeable.

Warning

Pasting a live production token into any website is a credential disclosure. This tool is client-side and sends your input nowhere, but the principle is worth stating plainly: when debugging, prefer an expired token or one issued by a staging environment.

base64url versus base64

JWT segments are encoded with the URL-safe variant defined in RFC 4648, not with standard Base64. Two things change: - replaces +, _ replaces /, and trailing = padding is dropped. The reason is practical, since tokens travel in URLs, query strings and HTTP headers where the standard alphabet would need re-encoding.

javascript
const payload = token.split('.')[1]; // Wrong: different alphabet, no paddingatob(payload);// InvalidCharacterError, or silently wrong output // Right: normalize the alphabet, then restore paddingconst normalized = payload.replace(/-/g, '+').replace(/_/g, '/');const padded = normalized.padEnd(  normalized.length + ((4 - (normalized.length % 4)) % 4),  '=');const binary = atob(padded);

If the length modulo 4 is 1, the input can never be valid Base64, because no encoder produces that shape. In practice it means the token was truncated during a copy. This tool reports that case with its own message, since the native atob error says nothing useful about what went wrong.

Non-ASCII claims and the atob trap

The binary value in the snippet above is not text. It is a binary string in which every character stands for one byte. With a pure ASCII payload you never notice. The moment a claim carries a name like "Kürşad" the output breaks, because characters such as ü and ş occupy two bytes in UTF-8 and a binary string presents those bytes as two separate characters.

javascript
// Treating atob output as text directlyatob('eyJuYW1lIjoiS8O8csWfYWQifQ');// '{"name":"KürÅŸad"}'   <-- mojibake // Decode the bytes explicitly as UTF-8const bytes = Uint8Array.from(atob(padded), c => c.charCodeAt(0));new TextDecoder('utf-8').decode(bytes);// '{"name":"Kürşad"}'

Any token carrying a user name, a city, a localized role label or a custom claim with non-ASCII text hits this. The failure is quiet: the mangled bytes still form valid JSON, so JSON.parse succeeds and you simply read wrong values. This tool decodes every segment through TextDecoder, so claim values render exactly as issued.

Time claims and clock skew

RFC 7519 defines exp, iat and nbf as NumericDate values: seconds elapsed since 1 January 1970. The JavaScript Date constructor expects milliseconds. Pass the raw claim straight in and you get a timestamp in the first days of 1970, which is the classic bug in homegrown JWT tooling.

ClaimMeaningConsequence when absent
expExpiry instant (seconds)The token never expires on its own, so a revocation list becomes mandatory
iatIssued-at instant (seconds)Token age cannot be computed, so no "too old" policy is possible
nbfNot valid before (seconds)The token is valid the moment it is issued
iss / audIssuer and intended audienceA token minted for another service may be accepted
jtiUnique token identifierSingle-use revocation and replay tracking get much harder
Time claims are in seconds, so multiply by 1000 before handing them to Date.

One more point deserves to be explicit: the "expired" verdict shown on this page is computed against your own device clock. On a laptop whose time has drifted, a perfectly valid token can look expired, or the reverse. Servers usually allow a few minutes of leeway for exactly this reason. If a result surprises you, check your system clock before you suspect the issuer.

JWS, JWE and alg none

Three dot-separated segments mean a JWS: header, payload and signature. Five segments mean a JWE, whose parts are header, encrypted key, IV, ciphertext and authentication tag. In a JWE the payload really is encrypted, so no tool can display it without the recipient decryption key. This decoder shows the header, states the reason, and does not print meaningless bytes in place of claims.

When the header declares alg as none, the token is unsigned. The specification allows it, but in practice it means anyone can mint a token with any claims they like. If your verification layer does not restrict accepted algorithms to an explicit allowlist, an attacker can strip the signature, set alg to none, and bypass authentication completely. The tool raises a warning whenever it sees such a header.

Frequently Asked Questions

Is the token I paste sent to a server?
No. Decoding runs entirely in JavaScript in your browser and no network request carries the token. Once the page has loaded you can disconnect and the tool keeps working. Even so, avoiding live production tokens while debugging remains a good habit.
Why is there no field for verifying the signature?
Verification needs the signing key, and that key should exist only on your own server. Building the habit of pasting secrets into a web page creates far more risk than the convenience is worth. Verify in your own environment with a library such as jsonwebtoken or jose.
The token looks expired here but the server accepts it. Why?
The expiry check is computed against your device clock. If your clock runs fast, a valid token appears expired. Servers also commonly allow a few minutes of clock-skew tolerance, so for tokens near the boundary the two sides can legitimately disagree.
Can I put sensitive data inside a JWT?
No. A JWS payload is only base64url encoded, not encrypted, so anyone who sees the token can read it. Carry an opaque reference id instead, or use JWE, where the payload genuinely is encrypted and this tool cannot display it either.
I decoded a token and the accented characters are garbled. What went wrong?
Almost always the atob output was treated as text. atob returns a binary string, so any character that takes two bytes in UTF-8 shows up as two separate characters. You need to decode the bytes with TextDecoder as UTF-8. This tool already does that, so the output here is correct.