JWT Decoder

Paste a JSON Web Token to read its header, claims and expiry. It decodes in your browser with no key — which is precisely why decoding tells you nothing about whether the token is genuine.

Decoding is not verifying. Everything below was read without any key, exactly as an attacker could. Nothing here tells you the token is genuine — only a signature check on your server can, using your secret or public key.

AlgorithmHS256
Issued at (iat)2018-01-18 01:30:22 UTC3147d ago

Decoding versus verifying

A signed JWT has three segments separated by dots: header, payload and signature. The first two are base64url-encoded JSON, and base64url is an encoding, not a cipher. Reversing it requires nothing — no secret, no public key, no permission. That is what this page does, and what any attacker holding your token can do just as easily.

Verification is a different operation entirely. It recomputes the signature over the header and payload using your secret (for HMAC algorithms such as HS256) or the issuer's public key (for RS256 and ES256) and compares the result. Only that step establishes that the contents arrived unaltered from someone holding the key. A claim you read out of a decoded payload is an assertion by whoever sent the token, and treating it as a fact is how authorisation bypasses happen.

The practical rule is short: decode in the browser to debug, verify on the server to decide. Client-side libraries named along the lines of jwt-decode exist only for the first purpose — they cannot verify, because doing so would require shipping your signing key to the browser.

Nothing in a JWT is private

Because the payload is merely encoded, every claim is world-readable from the moment the token leaves your server. The signature guarantees integrity, not confidentiality. Anything you would not print in a log file does not belong in a JWT: no passwords, no full personal records, no internal identifiers that leak system structure. If the contents genuinely must be hidden, JWE is the encrypted variant — recognisable because it has five segments rather than three.

The alg field is attacker-controlled

The header names the algorithm, and the header is part of the token, which means the sender chooses it. A verifier that reads alg and dispatches accordingly can be handed "alg": "none" with an empty signature and told there is nothing to check:

{"alg":"none","typ":"JWT"}.{"sub":"admin"}.
                                              ^ empty third segment

RFC 7519 permits unsecured JWTs, so this is a legal token rather than a malformed one — the vulnerability lives in the verifier, not the format. Pin the algorithms your server accepts instead of trusting the header, and reject none unconditionally. The related confusion attack swaps RS256 for HS256 so that a public key gets used as an HMAC secret; pinning defeats that too.

Time claims are in seconds

exp, nbf and iat are NumericDate values — seconds since the Unix epoch, not milliseconds. This trips up JavaScript constantly, because Date.now() returns milliseconds:

Math.floor(Date.now() / 1000)   // 1788101681   <- correct, 10 digits
Date.now()                      // 1788101681678 <- 13 digits, wrong claim

Passing the millisecond value produces an expiry roughly fifty thousand years out, and no validator will complain because the token is simply not expired. The decoder above renders all three claims as UTC times alongside how far away they are, which makes a mistake of this shape obvious at a glance.

Frequently asked questions

Does decoding a JWT mean it is valid?

No, and this is the single most consequential misunderstanding about JWTs. Decoding reverses base64url, which needs no key at all — we confirmed that a standard example token yields its algorithm and every claim, including the subject, with nothing but the token itself. Anyone can craft a token containing any claims they like, so a decoded field is an assertion by whoever sent it, never a fact. Verification is a separate cryptographic step: recomputing the signature over the header and payload with your secret or public key and comparing. If a value drives an authorisation decision, it must come from a verified token checked on your server, never from a decode.

Why are JWT claims readable by anyone?

Because a signed JWT is not encrypted. The three segments are base64url-encoded, which is an encoding rather than a cipher, so anyone holding the token can read the header and payload instantly — this tool does it in your browser with no key. The signature protects integrity, meaning it proves the contents were not altered after issuance, but it does nothing for confidentiality. Never put anything sensitive in a JWT payload: no passwords, no full personal records, no internal identifiers you would not publish. If the contents genuinely must be hidden, you need JWE, the encrypted variant, which has five segments rather than three. A useful test before adding any claim: would you be comfortable seeing it in a server access log, since a token in a URL frequently ends up in one.

What does alg: none mean and why is it dangerous?

It declares that the token carries no signature at all, and the third segment is empty. RFC 7519 permits unsecured JWTs, so the value is legal, and that is exactly the problem. A library that trusts the header's algorithm field to decide how to verify can be handed a token that simply asserts none, at which point it accepts anything an attacker writes. The defence is to pin the algorithms your server will accept rather than reading them from the token, and to reject none outright. Several widely used libraries shipped this vulnerability historically, and the tool above flags the header when it sees it. The same class of attack swaps RS256 for HS256 so that the issuer's public key — which is not secret — gets used as an HMAC secret, and pinning the algorithm defeats that variant too.

How do I read the exp and iat claims?

They are NumericDate values: seconds since 1970-01-01 UTC, not milliseconds. That off-by-a-thousand catches JavaScript developers constantly, because Date.now() returns milliseconds — at the time of writing the correct exp value has ten digits while Date.now() has thirteen. Passing a millisecond value produces an expiry tens of thousands of years away, which no validator will reject. The related claims are nbf, the earliest moment a token may be used, and iat, when it was issued. The tool above converts all three to readable UTC times and says whether the token currently falls inside its validity window, so a value in the wrong unit shows up immediately as an expiry tens of thousands of years away rather than passing silently. Note also that exp is optional in RFC 7519: a token without it never expires on its own.

Is the token I paste here sent anywhere?

No. This page is a static file and the decoding runs in your browser using the built-in atob and TextDecoder functions. There is no API call behind the box, no server-side logging, and no analytics event carrying your token, so the page keeps working with the network disconnected once loaded. That matters more here than on most tools, because a real JWT is often a live credential. Nothing is written to local storage either, so reloading clears it. The only network request the site makes at all is a cookie-less Cloudflare Web Analytics beacon that counts page views, and it carries the page URL rather than anything from the input box. That said, if the token you are debugging is a live production credential, rotating it afterwards is still sound practice.

References

Guides

JWT segments are base64url — the same encoding our Base64 tool handles. More browser-only tools at withuse.io/tools.