JWT Decoder & Inspector
Paste a JSON Web Token to read its header and payload, humanize the expiry dates and check its validity — decoded live in your browser, never uploaded.
Used only in your browser to recompute the signature. It is never uploaded, logged or stored — reload and it is gone.
For RS256/384/512, PS256/384/512 and ES256/384/512. Accepts an SPKI PEM or a JWK. Verified live in your browser via WebCrypto — the key and token are never uploaded. Verdict updates as you type; the Verify button re-runs it.
Your token and secret never leave this page. Decoding, and the HMAC signature check via your browser's built-in WebCrypto, all run locally in a small script you can read — no token or secret is ever uploaded. A JWT is a bearer credential, so working with it privately matters. (A cookieless page-view counter is the only network call the page makes; it never sees your token.)
| Claim | Value |
|---|
HMAC (HS*) tokens are checked with the secret; asymmetric tokens (RS*, PS*, ES*) with the pasted public key — both entirely in your browser via WebCrypto. If the key doesn't match the token's algorithm, curve or modulus, you get an honest can't verify, never a fake green or red. EdDSA has no universal WebCrypto support, so it stays server-side. alg:none is always flagged unsafe. A valid signature proves integrity only; still check exp, nbf and the issuer.
Create a test JWT — HMAC (HS256/384/512) only
Build a signed test token from your own claims, entirely in your browser. HMAC only: asymmetric (RS/PS/ES) signing needs the issuer's private key and is out of scope here — this page only verifies those. alg:none is never offered: an unsigned token is unsafe by construction.
A passphrase is UTF-8; a random API-issued key is usually hex or Base64 — decode it wrong and the signature is silently different, so the encoding is explicit here (same affordance as our HMAC generator). The secret is used only in your browser and never uploaded.
Every generated token is immediately round-tripped through this page's own decoder and HMAC verifier — the exact code path the panel above uses — so the green check is a real verification, not an echo. The header is always {"alg":…,"typ":"JWT"}; claims are signed exactly as you typed them, in your key order.
What a JSON Web Token is
A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe way to carry a set of claims — statements like "this is user 1234567890" or "this token expires on 1 Jan 2100" — between two parties. The form you see in an Authorization: Bearer … header is a JWS: three base64url strings joined by dots, header.payload.signature. Because it is self-contained and signed, a server can issue one and later trust it without a database lookup — provided it verifies the signature first. For a from-first-principles walkthrough of each part and the registered claims, learn more in our JWT structure guide.
A token decoded field by field
The sample token loaded above is a real, well-formed JWT. Split on the two dots, its first segment base64url-decodes to the header:
{ "alg": "HS256", "typ": "JWT" }
So it was signed with HMAC-SHA256 using a shared secret. The middle segment decodes to the payload:
{ "sub": "1234567890", "name": "Ada Lovelace", "role": "admin", "iss": "polyatic.com", "iat": 1516239022, "nbf": 1516239022, "exp": 4102444800 }
Reading it claim by claim: sub is the subject (user ID 1234567890); name and role are custom claims the issuer chose to include; iss says polyatic.com signed it. The three numbers are NumericDates — seconds since 1 January 1970 UTC. iat and nbf are both 1516239022, which is Thu 18 Jan 2018 01:30:22 UTC: issued and valid from that instant. exp is 4102444800 = Fri 01 Jan 2100 00:00:00 UTC, so the panel above shows valid — today is between nbf and exp. The third segment (R5smG_uC…) is the HMAC-SHA256 signature. Because this sample is signed with the demo secret polyatic-demo-secret (pre-filled in the secret box above), the panel also shows Signature valid — change a single character of the payload or the secret and it flips to invalid.
Decoding is not verifying
This is the single most important thing to understand. Decoding just base64url-decodes the header and payload — anyone can do it, no key required, which is exactly what happens here. Verifying recomputes the signature with the secret (HMAC) or the issuer's public key (RSA/ECDSA) and checks it matches. Notice that the payload above claims "role": "admin". Nothing stops an attacker copying this token, editing the payload, and re-encoding it — the decode still "works". Only signature verification catches that. A decoded payload alone is an unverified assertion, never proof.
Verifying an HMAC (HS256) signature here
For the symmetric HMAC algorithms — HS256, HS384, HS512 — verification needs only the one shared secret, so this tool can do it locally. Paste the token, type the secret, and the tool recomputes the signature exactly as a server would: it takes the two original base64url segments, joins them with a dot into the signing input (header.payload), runs HMAC with the SHA size named by alg using your secret as the key, base64url-encodes the result and compares it byte-for-byte with the token's third segment. A match means the token was signed with that exact secret and not altered since — shown as Signature valid. Any mismatch (wrong secret, or a tampered header/payload) shows Signature invalid. All of this uses your browser's built-in crypto.subtle; the secret never leaves the page. This catches the role-tampering attack above: edit the payload and the recomputed HMAC no longer matches.
Verifying an asymmetric (RS / PS / ES) signature here
Asymmetric tokens verify with the issuer's public key — a different key from the one that signed them — so this tool now checks them too, still entirely in your browser. Paste that public key into the Public key field as either an SPKI PEM (a -----BEGIN PUBLIC KEY----- block, the kind openssl … -pubout emits) or a JWK JSON object, and the tool imports it with crypto.subtle.importKey and runs crypto.subtle.verify against the token's signature. It supports the nine common algorithms: RS256/384/512 (RSASSA-PKCS1-v1.5), PS256/384/512 (RSA-PSS) and ES256/384/512 (ECDSA on P-256/P-384/P-521). For ECDSA the JOSE signature is the raw r‖s concatenation — 64, 96 or 132 bytes — which is exactly the form WebCrypto expects, so it is fed through unchanged with no DER conversion. Try the RS256 + public key and ES256 + public key chips above to watch a real token verify green.
Crucially, the verdict is honest. If you paste an EC key for an RS256 token, a key on the wrong curve, a malformed PEM, or a PS* token's key against an RS* token, the import or verify fails and the tool says can't verify: <reason> — it never turns a mismatch into a red "invalid" (which would wrongly imply the token was forged) or a fake green. EdDSA (Ed25519) has no universal WebCrypto support, so it is reported as unsupported rather than guessed. And alg:none tokens are always flagged unsafe: they carry no signature at all and must be rejected.
Creating a test token here
The create panel above builds a real, signed JWT from claims you type — useful when you need a known-good (or deliberately expired) token to feed your own middleware, an API gateway rule, or a parser test. It is scoped honestly to HMAC: HS256/384/512 sign with the same shared secret used to verify, which is exactly what you type into the secret box, and the signature is computed with your browser's built-in crypto.subtle — the secret never leaves the page. The header is always the canonical {"alg":…,"typ":"JWT"}; the payload is signed byte-for-byte as you wrote it, in your key order. The helper chips insert iat/nbf as the current instant and exp as one hour from now, so an "expired" test token is one edit away (set exp to any past NumericDate). Two things this panel will never do: sign with asymmetric algorithms — RS/PS/ES need the issuer's private key, which a browser page has no business handling, so this page only verifies those — and emit an alg:none token, which is unsigned and unsafe by construction. Each generated token is round-tripped through the page's own decoder and verifier, so the green check you see is the same verification a pasted token gets.
Signing algorithm reference
The alg in the header tells the recipient how to verify. The common values:
| alg | Family | Key model |
|---|---|---|
HS256 | HMAC-SHA256 | One shared secret signs & verifies |
HS384/512 | HMAC-SHA-384/512 | Same, longer digest |
RS256 | RSA PKCS#1 v1.5 | Private key signs, public verifies |
PS256 | RSA-PSS | As RS256, modern padding |
ES256 | ECDSA P-256 | EC private signs, public verifies |
EdDSA | Ed25519 | EC private signs, public verifies |
none | Unsecured | No signature — reject on servers |
Symmetric (HS*) means every party that can verify can also forge, so the secret must stay server-side. Asymmetric (RS*, ES*, EdDSA) lets many parties verify with a public key while only the issuer can sign — the right choice when tokens cross a trust boundary.
Common mistakes
Trusting a decoded payload for authorization. As shown above, anyone can rewrite the claims; act only after server-side verification. Putting secrets in the payload. A signed JWT is not encrypted — the payload is plain base64url, readable by anyone holding the token, so never store a password, card number, or API key in it. Accepting alg: none or letting alg pick the verifier. The classic attack switches an RS256 token to HS256 and signs it with the public key as the HMAC secret; pin the expected algorithm on the server. Ignoring exp — or omitting it. A token with no exp never expires on its own, which is usually a bug, not a feature. Confusing UTC with local time when a token seems "already expired": NumericDates are always UTC.
Honest limits of this tool
This decoder reads compact JWS tokens (the everyday case). It verifies HMAC (HS*) signatures when you supply the secret, and RSA/ECDSA ones (RS*, PS*, ES*) when you supply the issuer's public key as SPKI PEM or JWK. It does not verify EdDSA (no universal WebCrypto support) — it says so honestly rather than guessing — nor does it fetch keys from a JWKS URL for you (paste the key yourself), and it does not decrypt JWE (encrypted) tokens, which look different. The valid / expired / not-yet-valid claim verdict is derived purely from your device's clock against nbf and exp with zero leeway, so a wrong local time will skew it, and real servers usually allow a few seconds of clock skew this display does not. Everything runs in your browser; neither the token, the secret nor the key is uploaded.