JWT Decoder & Inspector

Paste a JWT to read its header and payload, with every timestamp turned into a date you can actually read. It runs in your tab, so the token is never sent anywhere, which matters more here than on most pages, because a JWT usually is the credential.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Decode. Nothing leaves this tab.

Wanted a different tool?

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

alg: none

A token asking to be trusted with no signature at all

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

An expired token

The exp claim read as a date, which is the check people do by eye and get wrong

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNTE2MjM5MDIyfQ.abc

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Trusting a decoded payload

    Decoding is not verification. Anyone can craft a token with any claims; only checking the signature against the expected key makes them meaningful.

    Instead:Verify the signature server-side, and reject alg: none outright.

  2. Putting personal data in the payload

    A JWT is base64, not encrypted. Anything in it is readable by anyone holding the token, including browser extensions and proxy logs.

    Instead:Carry an opaque subject id and look the rest up server-side.

  3. Relying on exp alone for logout

    A token stays valid until it expires. There is no way to revoke one without server-side state.

    Instead:Keep expiry short and pair with a refresh token you can revoke.

What it checks

Decoding a JWT is twenty lines of code. The useful part is what the claims add up to once they are readable.

Timestamps become dates

exp, nbf and iat are seconds since the epoch, which nobody can read at a glance. Each one is shown as an ISO date and as a plain phrase: expired two hours ago, valid for another six minutes, so the answer you came for is the first thing on the page.

It flags alg: none

The oldest JWT attack is to take a real token, edit the payload, set the algorithm to none and send it back. Any verifier that trusts the header's own algorithm accepts it. If a token arrives with alg: none, that is the loudest finding here.

Secrets in the payload

A JWT payload is base64, not encryption. Anyone holding the token reads every claim in it, and so does every proxy and access log on the way. Claims named like credentials are called out as critical, because if one is in there it should be treated as leaked.

It says what it did not check

Decoding proves what a token says, never that it is genuine. Verifying the signature means fetching the issuer's public keys, which is a network request this page does not make, so the report always ends by saying the signature was not verified.

What is a JWT, and what is actually inside one?

A JSON Web Token is three chunks of base64url joined by dots. It carries a set of claims about who someone is, and a signature that lets a server check the claims have not been altered, without calling back to whoever issued them. That last part is the whole point, and it is also where every JWT design problem comes from.

Three parts, separated by dots

The header names the signing algorithm. The payload holds the claims. The signature is computed over the first two parts together with a secret or a private key. Only the signature is genuinely opaque: the other two are ordinary JSON that anyone can read, which is what the tool above does.

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiJ9 . 3rXNPl7ZgqLm...
|__________________|   |______________|   |____________|
       header               payload          signature

header   {"alg":"HS256","typ":"JWT"}
payload  {"sub":"42","exp":1893456000}

The payload is encoded, not encrypted

This is the thing to internalise. base64url is not a cipher: it has no key and reversing it is a single function call. Every claim in a token is readable by the browser it was sent to, by every proxy it passes through, and by anything that logged the request. Putting a password, an API key or a national ID number in a payload is publishing it. If a token has ever contained one, treat that value as leaked and rotate it.

The signature proves integrity, not secrecy

A signature answers one question: has this token been altered since it was issued? It says nothing about whether the contents are private, because they are not. Verifying means recomputing the signature with the issuer's key and comparing, which is why verification belongs on your server, and why this page decodes but deliberately does not verify.

signature = HMAC-SHA256(
    base64url(header) + "." + base64url(payload),
    secret
)

change one character of the payload and the signature no longer matches

The claims that carry the security

Seven claim names are registered in RFC 7519 and the rest are yours. Three of them are timestamps, given as NumericDate: seconds since the Unix epoch, as a JSON number. Emitting those in milliseconds is a common bug and pushes the expiry into the year 55000, which reads as 'never expires' to anything checking it.

iss   issuer            who minted it
sub   subject           who it is about, usually the user id
aud   audience          which service is allowed to accept it
exp   expires at        seconds since 1970, NOT milliseconds
nbf   not valid before  seconds since 1970
iat   issued at         seconds since 1970
jti   token id          unique, for replay detection or a denylist

Why you cannot revoke one

The reason a JWT scales is that the verifier never has to ask anyone whether it is still valid: the signature and the expiry are enough. That is also why logging someone out does not invalidate their token. Once issued, it is accepted until it expires, so the gap between iat and exp is the real blast radius of a leak. Short-lived access tokens with a refresh token behind them is the standard answer; a denylist keyed on jti works but gives back the statelessness you chose a JWT for.

alg: none, and why verifiers pin the algorithm

The header names its own algorithm, which means an attacker gets to suggest how their token should be checked. Set alg to none, strip the signature, and a naive verifier that trusts the header will accept an edited payload. A related attack takes an RS256 public key, which is public, and submits a token signed HS256 using that key as the HMAC secret. Both are fixed the same way: decide server-side which algorithm you accept and refuse everything else.

# what a verifier should do
verify(token, key, algorithms=["RS256"])   # pinned

# what it should never do
verify(token, key)                          # trusts header.alg