JWT Token Decoder
Decode and inspect JSON Web Tokens.
Decoding only — the signature is not verified. Nothing is sent anywhere.
Overview
A JWT is three base64url-encoded parts separated by dots: a header saying how it was signed, a payload of claims, and a signature over the first two. The encoding is not encryption. Anyone holding the token can read every claim in it, which is worth internalising before you put anything in a payload.
This tool decodes the first two parts and formats them, then pulls out the three time claims and converts them from Unix seconds into timestamps you can read. It does not verify the signature — see below for why that is the right call rather than a missing feature.
How to use
- Paste the token. Everything after
Bearer— the scheme itself is not part of the JWT. - Read the header to see the algorithm and, if present, the key id.
- Read the payload for the claims.
- Check the timestamp table for
iat,nbf,expand the expiry verdict.
The registered claims
| Claim | Name | Notes |
|---|---|---|
iss | Issuer | Who minted the token. Your server should check this against an allowlist |
sub | Subject | Who the token is about. Usually a stable user id, not an email |
aud | Audience | Who the token is for. A token minted for another audience must be rejected |
exp | Expiration | Unix seconds. After this, reject |
nbf | Not before | Unix seconds. Before this, reject |
iat | Issued at | Unix seconds. Useful for “re-authenticate if older than N” |
jti | JWT ID | Unique id, so a token can be revoked or replay-checked |
All seven are optional in the spec. A token that omits exp never expires on
its own, which is a design decision worth noticing when you see it.
Why signature verification is not offered
Verifying a signature needs the key: the shared secret for HS256, or the
public key for RS256 and ES256. For the symmetric case, the key that
verifies is also the key that signs — so a page that offered verification
would be asking you to paste the credential that lets anyone mint tokens for
your system. There is no version of that which is safe to build.
Decoding is still worth doing. Most JWT debugging is “what does this token
actually say” — wrong audience, missing scope, an exp five minutes in the
past, a sub that is an email when your code expects a UUID. All of that is
visible without the key.
What decoding cannot tell you is whether the token is genuine. A JWT you received is a claim, not a fact, until your server checks the signature against a key it trusts.
The alg field is not a decision your verifier should delegate
The header’s alg describes what the sender used. A verifier that reads
alg and picks its algorithm accordingly is trusting attacker-controlled
input, which produces two classic breaks:
alg: none. The unsecured JWT is legal in the spec. A verifier that honours it accepts any payload with an empty signature.RS256swapped forHS256. If a library takes the public key and uses it as an HMAC secret, an attacker who knows your public key — it is public — can sign tokens you will accept.
Both are configuration problems, not cryptography problems. Pin the expected algorithm in your verifier and reject anything else, rather than reading it from the token.
Examples
- A 401 you cannot explain. Decode and check
aud. A token minted for one API and presented to another is the most common cause, and the error message rarely says so. - An intermittent 401. Check
expagainstiat. A short lifetime plus a client that caches the token is intermittent by construction. - A permission that should work. Look for
scopeorrolesin the payload. If the claim is absent, the problem is at the issuer, not in your authorization code. - Auditing what you put in a token. Decode one of your own. Anything in there — email address, internal ids, feature flags — is readable by whoever holds the token, including the browser it is stored in.
Where a token should live in a browser
This comes up every time someone decodes a token and notices it is readable. The short version: because a JWT is a bearer credential, anything that can read it can use it.
localStorage is readable by any script on the origin, which means an XSS
takes the token with it. It is also never sent automatically, so you cannot be
CSRF’d through it — you trade one class of bug for another.
An HttpOnly cookie cannot be read by script at all, so XSS cannot steal it.
It is sent automatically, so it needs SameSite=Lax or Strict plus
Secure, and any state-changing endpoint needs its own CSRF defence.
The usual conclusion for a browser app is a short-lived access token kept in
memory only, refreshed from an HttpOnly, Secure, SameSite refresh-token
cookie. That way an XSS gets a token with minutes of life rather than a
credential that lasts weeks, and the long-lived secret is never reachable from
JavaScript.
Whichever you pick, exp is doing the real work. A token with a one-hour
lifetime and no revocation list is valid for one hour after it is stolen, and
nothing on the client side changes that.
Notes
Padding is added before decoding, because base64url normally omits it. Tokens copied out of logs sometimes arrive with the padding intact, and both forms work here.
The expiry verdict uses your local clock, so it is a convenience rather than an
authority. If you need to compare against a specific moment, use the
Unix timestamp converter on the raw exp value.
Payloads are not always JSON in principle — the spec allows any content type — but every JWT in practice carries a JSON object, and a payload that fails to parse as JSON is displayed as raw text rather than hidden.