JWT Decoder
Local processing · verifiedDecode a JWT, read the claims, and actually verify the signature.
Decoding and verification both use the browser crypto API. Your token and secret stay on the device. How to verify this yourself.
This token's header says alg: none, so it carries no signature at all. Anyone can edit the
payload and the result will still decode. Treat it as untrusted input, never as an identity.
Header
—
Payload
—
Claims
| Claim | Value | Meaning |
|---|
Time claims
—
—
Signature
Decoding does not prove anything. Paste the key used to sign this token to check the signature; it stays in this tab and is never stored or sent.
No key supplied, so the signature has not been checked.
Decoding a JWT is not verifying it
Every JWT tutorial says "paste your token to see the claims", which teaches exactly the wrong reflex. The claims you see are just base64url-decoded JSON; nothing about them is trustworthy until the signature has been checked. This page separates the two steps on purpose, and splits the signature result into three states: verified, not matching, and not checked because no key was supplied.
A JWT is signed, not encrypted
A compact JWS is three base64url segments joined by dots: the header, the payload, and the signature. Base64url is an encoding — it maps bytes to a URL-safe alphabet and back — and it is not a cipher. Anyone who holds the token can recover the header and the payload without a key, which is exactly what the decoder above does. The signature adds integrity, not secrecy: it proves the first two segments have not been altered, and nothing more.
The practical consequence is a rule that is easy to state and easy to break: never place a secret in a claim. An email address, a role, a tenant id and a token identifier are all fine; a password, a private key, a one-time code or a full customer record are not. The moment a payload contains something you would not print on a postcard, the design is wrong — and the fact that the token looks like gibberish at a glance is what makes the mistake tempting.
Time claims, and the unit that catches people out
exp, nbf and iat are NumericDate values: seconds since the Unix epoch. Not
milliseconds, and not a formatted string. exp is the instant at or after which the token must be
rejected, and nbf is the instant before which it must be rejected. Because the unit is seconds, a
value that is accidentally treated as milliseconds lands in 1970 and a token appears long expired; treated the
other way, a value in milliseconds becomes a date tens of thousands of years in the future and never expires. The
page renders each of these as an ISO timestamp alongside the raw number, which makes a unit error visible
immediately.
Two subtler points. First, an absent exp is not safer — it means the token was issued without an
expiry at all, and only explicit revocation will retire it. Second, always allow a small clock-skew window when
checking nbf and exp: the issuer's clock and the verifier's clock are different machines,
and a few seconds of drift should not reject a perfectly good token or accept a just-expired one.
Why the header cannot be trusted either
The alg field lives in the header, and the header is attacker-controlled input that arrives with the
token. Several real vulnerabilities came from verifiers that read alg and followed it: accepting
alg: none, or treating an RS256 token as HS256 and verifying the "signature" with the public key as if
it were an HMAC secret. A correct verifier decides the allowed algorithms from configuration and rejects a token
whose header asks for anything else. This tool detects alg: none and says plainly that the token is
unsigned, rather than reporting a green check for a token that carries no signature.
The same principle explains why the signature result here has three states instead of two. "Verified" and "not verified" leave out the case that matters most in practice: no key was provided, so nothing was checked. Showing that state honestly is the difference between a decoder and a false sense of safety.
How the signature is actually checked, in the page
For HS256, HS384 and HS512 the pasted secret is imported as an HMAC key with crypto.subtle.importKey,
and the signature is checked over the exact ASCII bytes of header.payload. For RS and ES algorithms the
pasted JWK is imported with importKey('jwk', …) and verified with crypto.subtle.verify.
That final call is constant time, so the comparison does not leak how many leading bytes matched — comparing
signature bytes with === would.
The demo token is signed with the secret devcvt-demo, which is published here on purpose so you can try
the verifier without a real credential. Change one character of the secret and the result flips to "signature does
not match"; change one character of the payload and it does the same, which is the property that makes a signed
token worth anything. The key you type stays in the tab: it is not written to storage, not remembered, and not sent
anywhere, which is the only sane way to handle a signing secret in a browser tool.
Common questions
- Does decoding a token mean it is valid?
No, and the distinction is the whole point of this page. Decoding is just base64url and JSON parsing; anyone can do it to any token without any key. A token is only trustworthy once its signature has been checked against the right key, and this tool reports three separate outcomes — signature verified, signature does not match, and no key supplied — so an unverified token is never presented as if it were valid.
- Why does the payload look like readable text?
Because a JWT is signed, not encrypted. The header and payload are base64url-encoded, which is an encoding, not a cipher, so anyone holding the token can read every claim. Never put a password, an API key or personal data in a JWT payload expecting it to stay secret; treat the whole token as public and keep only identifiers and non-sensitive claims inside it.
- Is exp in seconds or milliseconds?
Seconds, always, and it is a NumericDate per RFC 7519 — the number of seconds since the Unix epoch, not milliseconds. A ten-digit value is seconds and becomes year 1970 if it is ever multiplied or treated as milliseconds by mistake. The page converts exp, nbf and iat to an ISO timestamp and to relative wording so a value in the wrong unit is obvious.
- Why can I not paste a PEM file to verify an RS256 token?
Because a browser cannot import a PEM block without parsing the ASN.1 structure inside it, and this tool does not ship a PEM parser. Web Crypto imports asymmetric keys as JWK, so paste the JSON web key instead. The tool detects a PEM block and says so explicitly rather than failing silently or pretending to verify.
- Is my token or secret uploaded anywhere?
No. Decoding uses plain string and JSON work in the page, and signature verification uses the Web Crypto API in the same tab. The key field is never written to local storage and is not remembered between visits. Open the network panel while you decode and verify: no request carries the token or the key.