Guide

How to decode a JWT

This decoder is for the compact JWS-style form: header, payload and signature as Base64url segments separated by dots. It reads the header and payload without a key because those segments are encoded, not encrypted. It can display an incomplete two-segment input with a warning, but a complete token for signing verification normally has all three segments.

That last point is the one worth internalising. Anyone holding a JWS can read the displayed header and payload. The signature can prove the token has not been altered after a verifier checks it; it does not hide the contents from anybody. A five-segment compact JWE is encrypted and this tool rejects it rather than trying to decode its protected payload.

Open the JWT Decoder

Use this when


  • An API returns 401 and you need to see why the token was rejected
  • Checking whether a token has expired, and when it was issued
  • Confirming which scopes, roles or permissions a token actually carries
  • Verifying that a login flow issued the claims you expected
  • Reading the `kid` to work out which signing key a service should use

The three segments


The header is a small JSON object naming the signing algorithm in `alg` and the token type in `typ`. It often carries a `kid`, a key identifier telling the verifier which of several public keys to use, useful when a provider rotates keys, and the first thing to check when verification fails against a key you believe is correct.

The payload holds the claims. Some are registered by the JWT specification and mean the same thing everywhere; the rest are whatever the issuer chose to include. Both are just JSON, so a payload can be any size, which is the usual reason a token grows large enough to hit a header size limit.

For a signed compact JWS, the signature is computed over the encoded header and payload together. Because it covers the encoded form rather than the decoded one, re-serialising the JSON and re-encoding it produces a different string and invalidates the signature, which is why a token must be passed around exactly as issued, byte for byte. This decoder does not verify that signature.

The registered claims that matter


`exp` is the expiry, `iat` the issue time, and `nbf` the earliest time the token is valid. All three are Unix timestamps in seconds, not milliseconds, the single most common mistake when generating tokens in JavaScript, where `Date.now()` returns milliseconds and produces an expiry roughly fifty thousand years in the future.

`iss` names the issuer and `aud` the intended audience. A verifier is supposed to check both: a token legitimately issued by the right provider for a different application is still not valid for yours, and accepting it anyway is a real vulnerability rather than a technicality.

`sub` identifies the subject, usually the user, and `jti` is a unique token id used for revocation lists. Neither is required, and how a given system populates `sub` varies enough that it is worth confirming rather than assuming it is a database id.

Decoding is not verifying


Decoding reads the token. Verifying checks the signature against the issuer's key, and confirms the expiry, the issuer and the audience. A decoder can show you a token whose signature is complete nonsense and the claims will display perfectly, because reading Base64 does not involve the key at all.

This is why no client application should make a trust decision from a decoded payload. Reading `role: admin` from a token in browser JavaScript tells you what the token says, and a token can say anything if nobody checked the signature. Authorisation belongs on the server, after verification.

Using a decoded payload for display is fine and normal, showing a user's name from the token rather than making another request. The line is between displaying a claim and relying on it.

Algorithms, and the attacks on them


HS256 is symmetric: the same secret signs and verifies, so every party that can verify can also forge. RS256 and ES256 are asymmetric (a private key signs, a public key verifies), which is what you want when tokens are verified by services that should not be able to issue them.

The `alg: none` attack is the historical reason to be careful. A token claiming no algorithm, with an empty signature, was accepted by early libraries that trusted the header to say how to verify. A verifier must decide the expected algorithm itself and reject anything else, rather than reading it from the token it is checking.

The related confusion attack takes an RS256 system, changes the header to HS256, and signs with the public key as the HMAC secret, which works if the library picks its algorithm from the header and the public key is, as intended, public. Both attacks are fixed in current libraries and both are worth recognising in a token that looks odd.

What should never be in a payload


Because the payload is readable by anyone holding the token, it must not contain anything confidential: no passwords, no API keys, no personal data beyond what the application needs, no internal identifiers you would not expose. This happens regularly, and Base64 looking opaque is exactly why.

Tokens are also frequently stored in places that leak, browser local storage, log files, error reports, analytics payloads, and URLs. A token in a query string ends up in server logs and browser history, and a token in local storage is readable by any script that achieves XSS on the page.

Size is the practical constraint that follows. Every claim is sent with every request, and many servers cap header size at 8 KB; a token stuffed with permissions can exceed it and produce a confusing 431 or 400 rather than an authentication error.

Common problems


Each of these is something that actually happens, with the cause rather than a generic suggestion to check your input.

The token decodes but the API rejects it

Cause
Decoding never checks the signature, the expiry, the issuer or the audience. Any of those can be wrong on a token that reads perfectly.
Fix
Check `exp` against the current time first, then `iss` and `aud`. If all three look right, the signature or the signing key is the problem.

A segment will not Base64-decode

Cause
JWT uses Base64url, which substitutes `-` and `_` and usually drops the `=` padding.
Fix
Convert `-` to `+` and `_` to `/`, then pad to a multiple of four. A JWT-aware decoder does this for you.

The expiry is decades in the future

Cause
`exp` was set from a millisecond timestamp. JWT timestamps are in seconds.
Fix
Divide by 1000 when issuing. In JavaScript that is `Math.floor(Date.now() / 1000)`.

A token expires immediately, or is not yet valid

Cause
Clock skew between the issuing and verifying machines, or an `nbf` in the future.
Fix
Synchronise clocks with NTP and allow a small leeway, most libraries support a few seconds of tolerance.

Requests fail with a 431 or an empty response

Cause
The token has grown past the server header limit, commonly 8 KB.
Fix
Move bulky claims out of the token and look them up server-side by `sub`.

Questions


Is a JWT encrypted?
A signed JWS is not encrypted: its header and payload are Base64url-encoded, which anyone can reverse. An encrypted JWE has five compact segments and is not readable here without its key. A JWS signature detects tampering only after verification; it does not provide confidentiality. Assume every displayed claim is public.
Can I trust the claims after decoding?
Not without verifying the signature. A decoder will happily display the payload of a forged token. Trust decisions belong on the server after verification against the issuer's key.
Why is the expiry a large number?
It is a Unix timestamp in seconds since 1 January 1970. If it looks a thousand times too large, it was generated from a millisecond value, a frequent bug in JavaScript code.
Is my token sent anywhere when I decode it?
No. Decoding happens entirely in your browser. That matters here more than almost anywhere else on this site, because a real access token is a live credential. The decoder does not transmit your token or decoded output; decoding does not use an upload endpoint.