JWT Decoder and Signature Verifier

Split a JWT into header, payload and signature, read the time claims as dates, and verify an HS256 signature locally so the secret never leaves the page.

A token from a login endpoint is an opaque string until you split it open: which user it belongs to, when it expires, whether the signature actually matches. Doing Base64URL by hand is tedious and easy to get wrong at the - and _ characters, and a hosted decoder means pasting production credentials into someone else's server.

Everything here runs in the page: the segments are decoded with the browser's built-in atob and TextDecoder, and HS256 verification uses Web Crypto HMAC-SHA256 with a secret that lives only in memory and disappears on reload. One caveat up front: a token that decodes cleanly and a signature that matches mean the structure is sound and the content is unaltered - they do not mean a server will accept the token.

The three segments and how they are built

A JWT is header.payload.signature, joined by dots. The first two segments are JSON encoded with Base64URL, which is ordinary Base64 with + replaced by -, / replaced by _ and the trailing = removed so the result survives a URL or an HTTP header untouched. The third segment is the signature over the exact text of header.payload - including the dot - computed with the algorithm named in the header: HS256 uses a shared secret, RS256 a private key. A server recomputes the same value from the token it receives and compares, which is how tampering is detected.

The payload is encoded, not encrypted

Base64URL involves no key, so anyone holding the token can read every claim in it - the single most common misconception about JWTs. Passwords, phone numbers, national ID numbers and internal hostnames do not belong in the payload; if a claim has to stay confidential, encrypt it in the application layer or leave it out of the token. exp, nbf and iat are Unix timestamps in seconds, and checking exp alone is not enough: a server should also validate iss and aud, restrict the accepted algorithms with an allowlist so a token rewritten to alg none is rejected, and treat the signature as the only thing that ties the content to a key holder.

Why verification has to happen locally

Verifying HS256 takes two inputs, the token and the shared secret, and a web service that asks for both now has your signing key - the ability to mint valid tokens for your system has been handed to a third party. Web Crypto runs HMAC-SHA256 inside this page, so the secret stays in memory and goes nowhere else. Keep the limits in view: a matching signature proves the content was not modified and that whoever signed it held the same secret. It does not prove the issuer is trustworthy, that the token has not expired, or that the server has not revoked it. JWTs are stateless, so a token stays valid until it expires unless the server maintains its own revocation list.

Advertisement

Frequently asked questions

Is the JWT payload encrypted?
No. It is Base64URL-encoded JSON with no key involved, so anyone can paste the token into a decoder and read every claim. Confidential data has to be encrypted before it goes into the token, or kept out of it entirely.
Does a valid signature mean the token is valid?
Not by itself. Verification only shows that the content was not altered and that the signer held the key. Whether the token may be used still depends on exp, nbf, iss and aud, and on whether the server has revoked it. Because JWTs are stateless, a token cannot be invalidated unilaterally once issued.
Why does HS256 verification keep failing?
Three usual causes: the secret is wrong or interpreted differently - raw text on one side, Base64-decoded bytes on the other; the algorithm does not match, and an RS256 token simply cannot be checked with HS256; or the token was copied with a line break, or with + and / where Base64URL expects - and _.
Why is exp a number instead of a date?
exp, nbf and iat are Unix timestamps in seconds since 1970-01-01 UTC, which is easy to compare in any language. Some runtimes hand you milliseconds, so divide by 1000 before converting. No time zone is stored; showing the result in local time is only a formatting step.

Related tools

Advertisement