JWT Decoder

Paste a token and get the header, the payload, every time claim rendered as a date with a verdict, and a real signature check. HS256, HS384 and HS512 verify against a pasted secret; RS256, PS256 and ES256 verify against a pasted PEM public key. All of it happens in your tab, with no network request in the code.

Try an example loads the published reference token, whose secret is public so the verify box has something true to show.

Verification is the point, and it has to be local

Every free JWT tool decodes. Decoding is base64 and a JSON parse, and it proves nothing: a token you decoded is a token you read, not a token you trust. The question that actually matters, "is this signature valid", is the one most tools skip.

The ones that do verify usually send the token to a server. Think about what that means. A JWT is a bearer credential: whoever holds it is, for its lifetime, the user it names. Pasting a live token into a box that transmits it is handing somebody your session, and doing it with the secret alongside is handing them the ability to mint more.

WebCrypto is in every browser, so there is no reason for a round trip. This page imports the key and verifies the signature in your tab. There is no upload endpoint and no fetch call in the code that does the work; you can watch your network panel stay silent while it runs.

What the output tells you

Four blocks. Here is the reference token, verified against its published secret:

HEADER
{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

TIME CLAIMS
iat (issued at): 2018-01-18T01:30:22Z, 8 years ago

SIGNATURE
Verified. The HS256 signature matches that secret.

The summary strip above it reads HS256 · 3 claims · signature verified. When a token is expired the strip says so, and when the signature does not match it says signature does NOT match in the same place, because that is the line people read.

Time claims, as dates and as a verdict

A Unix integer tells you nothing at a glance. 1788445805 is a number; whether the token is live is the question. So every recognized time claim is rendered three ways: the raw seconds, an ISO date, and a plain sentence.

ClaimMeansVerdict reads
expexpires"valid for another 41 minutes", or "expired 3 days ago"
nbfnot valid before"not usable for another 12 seconds", or "usable since 2 hours ago"
iatissued at"8 years ago"
auth_timeauthenticated at"3 minutes ago"

An expired token also raises a warning above the result, because "expired 3 days ago" buried in a list is easy to skim past when you are debugging a 401 and looking for something more exotic.

One quiet fix worth knowing about: a value past the year 5138 read as seconds is a millisecond timestamp somebody forgot to divide, which is a genuinely common bug in token-issuing code. It is detected and converted, so the date you see is the date that was meant, and the anomaly is visible rather than showing you a nonsense year.

Which algorithms verify, and what you need for each

  • HS256, HS384, HS512: symmetric, HMAC with SHA-256, 384 or 512. Paste the shared secret into the secret box. The same secret both signs and verifies, so treat it as you would a password.
  • RS256, RS384, RS512: RSA with PKCS#1 v1.5 padding. Paste the PEM public key. The public key cannot mint tokens, so this is the safe one to paste.
  • PS256, PS384, PS512: RSA with PSS padding, same key format.
  • ES256, ES384, ES512: ECDSA over P-256, P-384 and P-521. Same PEM public key format.

The PEM must be a public key block, beginning -----BEGIN PUBLIC KEY-----. If you have a certificate rather than a bare key, extract the public key from it first; if you have a JWKS endpoint's JSON, convert the JWK to PEM. Anything that is not a recognizable PEM block gets a sentence saying so rather than a confusing crypto error.

Leave the boxes empty and the page says the signature was not checked. It does not say "valid". A tool that reports a green tick when it verified nothing is worse than one that does not verify at all.

alg: none is a finding, not a pass

A token whose header says "alg": "none" carries no signature. The specification permits it for cases where integrity is guaranteed by some other means, and in practice it is the oldest JWT attack there is: take a valid token, change the payload, set the algorithm to none, drop the signature, and any library that honors the header accepts it.

So this page reports none as not verified, with a sentence explaining that any system accepting the token accepts one anybody can forge. Some tools show it as a successful decode with nothing to check. That is technically accurate and it buries the most important thing on the screen.

When a token will not decode at all

  • Two segments, or four. A JWT has three parts separated by dots. Two usually means the signature was cut off in a copy; the error says how many it found.
  • Five segments. That is a JWE, which is encrypted rather than signed. There is nothing to read without the decryption key, and the error says so rather than producing gibberish.
  • A whole Authorization header. A leading Bearer is stripped automatically, so pasting the header works.
  • A segment that is not base64url. Padding is tolerated in either direction, so the usual cause is a line break or a space inserted by whatever you copied from.

A decoded segment that is valid base64 but not JSON gets the first eighty characters quoted back, which is normally enough to see that it is a different token format entirely.

Frequently Asked Questions

Is it safe to paste a real token here?

Safer than anywhere that transmits it, because this page does not. There is no upload endpoint and no network call in the code that does the work; the decoding and the WebCrypto verification both happen in your tab. That said, a live token is a live credential, so the safest habit anywhere is to use an expired one when you can.

What about the secret? Is that transmitted?

No. It is read from the box, imported as a WebCrypto key in the page, used to verify, and never leaves. Nothing is stored between visits either, so reloading gives you an empty box. If you would rather not paste a signing secret into any web page at all, that is a reasonable position, and the decode half of this page works without it.

Why does it say the signature was not checked?

Because no secret or key was given for an algorithm that needs one. That is reported as "not checked" rather than as a pass, deliberately: an unchecked signature is an unknown, not a success.

My RS256 key will not import.

It needs to be a PEM public key block, starting -----BEGIN PUBLIC KEY-----, which is the SubjectPublicKeyInfo format. A certificate block, an -----BEGIN RSA PUBLIC KEY----- PKCS#1 block, or raw JWK JSON all need converting first. The error names what it expected.

Can it verify against a JWKS URL?

No, because fetching one would mean a network request, and the whole premise of the page is that it makes none. Fetch the JWKS yourself, pick the key matching the token's kid, convert it to PEM and paste that.

Does a verified signature mean the token is valid?

It means the token was signed by whoever holds that key and has not been altered since. Whether it is valid also depends on the expiry, the not-before time, the issuer, the audience and whatever your application requires. The page shows you the time claims and the whole payload so you can check the rest yourself.

Does anything I paste leave my computer?

No. There is no upload endpoint on this page and no network request in the code that does the work. JavaScript in your own tab reads the text, processes it and hands back the result. Nothing is stored between visits either, so reloading gives you an empty box again. You can confirm it by opening your browser's network panel and watching it stay quiet while you work.

Decode it, then actually check it

Free, no account, nothing transmitted. HMAC, RSA and ECDSA verified in your own tab.

Back to the decoder