A JWT is three base64url-encoded segments joined by dots: a header naming the algorithm, a payload of claims, and a signature that proves the first two segments were not tampered with after the token was issued. Decoding shows you what the token says. Verification proves it is still trustworthy. This tool does both, runs entirely in your browser, and never generates or signs a token — there is no key-bearing path here for an attacker to target.
Three segments, two of them readable
RFC 7519 defines the JWT compact form. The header and payload are JSON objects, individually base64url-encoded per RFC 4648 §5 — the URL-safe variant that swaps + for -, / for _, and drops the = padding. The signature is the MAC or digital signature over the concatenated header.payload string, itself base64url-encoded. Run decodeJwt on the canonical RFC 7519 §3.1 example and you get a header of { "typ": "JWT", "alg": "HS256" } and a payload of { "iss": "joe", "exp": 1300819380, "http://example.com/is_root": true }.
base64url is not encryption. Anyone who can read the URL can read the payload — that is the whole point. JWTs are tamper-evident, not confidential. If you put a session secret, a phone number, or an internal user ID inside the payload, you have published it. Use JWE (RFC 7516) if you need confidentiality. This decoder reads JWS only; encrypted tokens (JWE) are out of scope.
Registered claims and what they actually mean
The seven IANA-registered claims (RFC 7519 §4.1) are the only ones with portable semantics across libraries: iss (issuer), sub (subject), aud (audience), exp (expiration, NumericDate seconds since epoch), nbf (not-before), iat (issued-at), and jti (unique token ID for replay defence). Everything else is a public or private claim — the spec is deliberate about not over-prescribing. The widget renders the registered set first in RFC order, then your custom claims in insertion order, and formats exp / iat / nbf as ISO timestamps with a relative-time hint so an expired token leaps off the page.
A token is expired when exp × 1000 ≤ now. The check is local to your machine and to the moment you opened the page — there is no time-skew negotiation. If your service rejects a token your decoder shows as valid by a few seconds, the difference is your server clock versus the issuer's, not a bug in the decoder.
HS, RS, PS, ES — and why the alg matters
RFC 7518 catalogues every algorithm a JWS may use. The three families that matter in practice are HMAC (HS256, HS384, HS512) using a shared symmetric secret; RSASSA-PKCS1-v1_5 (RS256, RS384, RS512) and its PSS-padded counterpart (PS256, PS384, PS512) using an RSA keypair; and ECDSA (ES256 on the P-256 curve, ES384 on P-384) using an elliptic-curve keypair. Symmetric HMAC is the easiest to deploy — both sides hold the same secret. Asymmetric variants are necessary when the verifier should not be able to issue tokens, which is the usual case in OAuth-style architectures: the authorisation server signs with a private key and resource servers verify with the matching public key.
The verify panel routes by family. HMAC takes the secret as raw bytes via SubtleCrypto.importKey("raw", …). RSA, RSA-PSS, and ECDSA take a PEM-encoded public key, which the widget strips of its -----BEGIN PUBLIC KEY----- armour, base64-decodes to SPKI DER bytes, and hands to importKey("spki", …). Private keys are rejected at the parser — you cannot mis-paste a signing key into a verifier.
Two well-known attacks the decoder will not hide from you
The first is alg=none. A JWT with {"alg":"none"} in the header carries no signature at all (the third segment is empty). The original spec allows it for unsigned context, but for production tokens it is a vulnerability: a library that "verifies" an alg=none token by skipping the signature check accepts anything. The OWASP JWT cheat-sheet has flagged this since 2015 and PortSwigger's Web Security Academy still uses it as a teaching exploit. The widget shows a red INSECURE banner the moment the header parses as none, and the verify pipeline refuses to claim a result.
The second is algorithm confusion. A vulnerable verifier that switches between RS256 (asymmetric, RSA public key) and HS256 (symmetric, shared secret) based purely on the token's self-declared alg can be tricked into using the public RSA key bytes as an HMAC secret. Anyone who can read the public key can then forge tokens. The fix on the server side is a strict allow-list: a verifier configured for RS256 should treat HS256 tokens as malformed, full stop. This decoder shows you the declared alg, the family it routes to, and what your key material is interpreted as — so you can spot the mismatch immediately.
Both attacks are catalogued in RFC 8725 — JWT Best Current Practices (BCP 225, published February 2020), which is the document the IETF wrote specifically to capture the lessons of a decade of JWT deployment. Section 2.1 mandates an explicit algorithm allow-list at the verifier; §3.1 deprecates alg=none outside well-defined trust boundaries; §3.6 warns about the polymorphism that enables algorithm confusion. Any production-grade JWT library written after 2020 either implements those recommendations by default or surfaces them in its README. If yours does neither, replace it.
Why Web Crypto, why client-side
Web Crypto SubtleCrypto ships in every evergreen browser and in Node 18+. It is the same primitive your authentication library uses on the server, accessible from the page. The verify call is one subtle.verify() against a key returned by importKey(), with the math running in the host's native crypto implementation, not in JavaScript. That buys constant-time comparisons against timing attacks on the signature check, hardware acceleration on platforms that have it, and a single audit surface across browsers and Node.
The decoder uses Web Crypto for verification only. There is no signing path in the source — search the repo for subtle.sign and you will only find it in test files generating round-trip vectors for the verifier. Your secret or private key, were you to paste one by mistake, would live in component-local React state for the lifetime of the tab and is never written to localStorage, sessionStorage, or any network egress.
What the page does with your token
The JWT input, the secret, and the public key are processed entirely in the browser tab. The decoded header, payload, and signature live in component state. The recent- inputs panel persists the JWT string itself — base64url is not a secret, it is publicly readable bytes — under the scope key recent-inputs:jwt-decoder:v1, alongside a variant tag (decoded or decoded-verified) and a timestamp. The verification secret and any pasted PEM key are never written to that storage; a runtime test in the suite sweeps the entire localStorage after a verify call to assert the secret's bytes are nowhere on disk. Recalling a history entry replays the JWT only — the secret field is wiped and you have to re-enter it.
The OAuth 2.0 access-token profile (RFC 9068)
For production OAuth deployments, RFC 9068 (October 2021) defines the JWT Profile for OAuth 2.0 Access Tokens. It is the spec that pins down which claims an access token must carry — iss matching the authorisation server, aud identifying the resource server, exp set short (the IETF recommends minutes, not days), and a client_id claim binding the token to the OAuth client that requested it. It also requires the typheader to be at+jwt so a verifier can distinguish access tokens from ID tokens, refresh tokens, and bespoke service-to-service JWTs that may share the same signing key. When you decode an access token here and the typ reads JWT instead of at+jwt, you are looking at a legacy issuer or a custom profile — neither is necessarily wrong, but it is worth knowing before you assume RFC 9068 semantics.
Where this earns its keep
The typical debugging session is two minutes: paste a token from an HTTP header, read the claims, check whether exp has passed, copy sub into a database query. Sibling tools cover the encoding tier — the Base64 Encoder/Decoder is the same shared base64url foundation behind this decoder, and the Hash Generator is what the HMAC family on this page is calling under the hood. For verification flows in CI, copy your signing secret or public key in, run the JWT, and the verify panel will tell you whether the signature is intact before you ship a change that rotates a key.

