About JWT Signature Verifier
Verification is the operation that decoding is not: it recomputes the signature over the header and payload with a key and checks that it matches. Only a successful verification against a key you trust tells you a token is genuine and unmodified.
Which key depends on the algorithm family. HS256, HS384 and HS512 are HMAC with a shared secret - the same secret signs and verifies, so anyone who can verify can also forge. RS256, RS384, RS512 and the ES and PS families are asymmetric: the issuer signs with a private key and you verify with the public key, which is what makes them the right choice when the signer and verifier are different parties.
The classic vulnerability here is algorithm confusion. A verifier that reads alg out of the token and trusts it can be attacked two ways: setting alg to 'none' to strip the signature entirely, or switching an RS256 token to HS256 so the library uses the public key - which is not secret - as an HMAC secret. Always pass an explicit allowlist of algorithms and ignore what the token asks for.
A valid signature is necessary but not sufficient. You still have to check exp and nbf against the current time, iss against the issuer you expect, and aud against your own identifier - otherwise a perfectly signed token issued for a different service, or one that expired last month, sails through.
The registered claims are worth knowing by name: iss (issuer), sub (subject - usually the user id), aud (audience - which API the token is for), exp (expiry), nbf (not before), iat (issued at) and jti (a unique token id). exp, nbf and iat are NumericDate values - seconds since the Unix epoch, not milliseconds - and mixing up the unit is one of the most common causes of a token that is somehow always expired or never valid.
Verification here runs in your browser using the Web Crypto API. The token and the key are never uploaded, though as always a throwaway key is a better habit than a production secret in any web page.
How to use the JWT Signature Verifier
- Paste the token, and select the algorithm you expect it to use rather than accepting whatever its header claims.
- Provide the key: the shared secret for HS*, or the PEM public key for RS*, ES* and PS*.
- Verify. The result tells you whether the signature matches and whether exp and nbf are currently satisfied.
- Check iss and aud by eye against what your service expects - a valid signature from the wrong issuer is still a rejection.
Examples
-
Sample HS256 JWT (secret: your-256-bit-secret)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT Signature Verifier in code
The same operation this tool performs, in the languages you are most likely to need it.
const enc = new TextEncoder();
const [h, p, s] = token.split(".");
const key = await crypto.subtle.importKey(
"raw", enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["verify"]
);
// Base64URL -> bytes
const b64url = (str) => Uint8Array.from(
atob(str.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)
);
const ok = await crypto.subtle.verify(
"HMAC", key, b64url(s), enc.encode(`${h}.${p}`)
);
console.log(ok); // signature only - you must still check exp/iss/aud
import jwt from "jsonwebtoken";
try {
const claims = jwt.verify(token, publicKey, {
algorithms: ["RS256"], // NEVER omit this
issuer: "https://auth.example.com",
audience: "https://api.example.com",
clockTolerance: 5, // seconds, for clock skew
});
// claims are now safe to act on
} catch (err) {
// TokenExpiredError | JsonWebTokenError | NotBeforeError
if (err.name === "TokenExpiredError") { /* refresh */ }
throw err;
}
// Omitting the "algorithms" option is the alg-confusion
// vulnerability: an attacker re-signs an RS256 token as HS256
// using your public key as the HMAC secret, and a lenient
// verifier accepts it.
import jwt
from jwt import PyJWKClient
# Fetch the issuer's public keys and pick the one matching the token's kid
jwks = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
signing_key = jwks.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"], # explicit allowlist
issuer="https://auth.example.com",
audience="https://api.example.com",
leeway=5, # clock skew tolerance, seconds
)
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.*;
// The algorithm is fixed by the verifier, not read from the token.
Algorithm alg = Algorithm.RSA256((RSAPublicKey) publicKey, null);
JWTVerifier verifier = JWT.require(alg)
.withIssuer("https://auth.example.com")
.withAudience("https://api.example.com")
.acceptLeeway(5)
.build();
DecodedJWT jwt = verifier.verify(token); // throws JWTVerificationException
When you need this
- Confirming a token your service rejects really is invalid, rather than a configuration problem on your side.
- Checking you have the right shared secret or public key for an issuer.
- Reproducing an expiry or not-before failure to see the exact times involved.
- Testing that a token signed by a new key rotates correctly before you deploy.
- Demonstrating that a tampered payload fails verification.
Common problems and what causes them
- "invalid signature"
- The key does not match, or the signed input is not what you think. Check: the right key for the token's kid; the secret decoded from Base64/hex rather than used as text; the whole header.payload string signed, not just the payload; and that nothing re-encoded the token in transport.
- "jwt expired" / TokenExpiredError
- exp is in the past. Compare exp against the current time in seconds. If it is only slightly past, the cause is usually clock skew between machines - allow a few seconds of tolerance rather than widening exp.
- Accepting the token's own "alg" value
- This is the alg-confusion vulnerability. An attacker sets alg to none to drop the signature, or downgrades RS256 to HS256 so your public key gets used as an HMAC secret. Always pass an explicit algorithm allowlist.
- Verifying the signature but not the claims
- A correctly signed token from a different issuer, or one issued for a different audience, is still not valid for your API. Check iss and aud explicitly - many libraries skip both unless you ask.
- HS256 secret used as text when it is Base64
- Several identity providers give the shared secret as Base64. Using the printable string as the HMAC key produces a different key and a failed verification. Decode it to bytes first.
- No revocation path
- Verification proves a token was issued and has not expired - not that it is still wanted. A stolen token stays valid until exp, so keep access-token lifetimes short and track jti or a token version if you need real revocation.
FAQ
- What is the difference between decoding and verifying a JWT?
- Decoding is Base64URL and needs no key - it shows you the claims but proves nothing. Verifying recomputes the signature with a key you trust and proves the token was issued by the holder of that key and has not been altered. Only verification supports an authorisation decision.
- HS256 or RS256 - which should I use?
- HS256 when the same party signs and verifies, since the shared secret is symmetric and anyone who can verify can also forge. RS256 when they differ - an identity provider signing tokens that many services verify - because those services only need the public key, which can be published safely.
- Why does verification fail when the token looks correct?
- Most often the wrong key (check kid against the issuer's JWKS), a secret used as text rather than decoded from Base64, or clock skew on exp/nbf. Also confirm the library is verifying header.payload and not something else.
- Why is accepting alg="none" dangerous?
- It tells the verifier there is no signature to check, so an attacker can present any claims they like. No production verifier should ever accept it - which is why you pass an explicit allowlist instead of trusting the token's header.
- How much clock skew should I allow?
- A few seconds - typically 5 to 30 - is standard and covers ordinary drift between servers. Large tolerances defeat the purpose of exp, so fix NTP rather than widening the window.
- Is it safe to paste a production secret here?
- The verification runs entirely in your browser and nothing is transmitted. Even so, treat any production signing key as something that should not be pasted into a web page - test with a throwaway key where you can.
- Why does it only support HS256?
- HS256 verification only needs the shared secret, which is safe to demonstrate client-side. RS256/ES256 verification needs the issuer's actual public key infrastructure, which is a different (and riskier to fake) workflow - use your backend's auth library for those.
- What does 'expired' mean in the result?
- It checks the token's exp claim (if present) against your current browser clock and reports whether the token has already expired - independent of whether the signature itself is valid.
- Is my secret uploaded anywhere?
- No. The HMAC is computed locally with the Web Crypto API; nothing you paste leaves your browser tab.
Related reading
- What is a JWT and how it works
- JWT header, payload and signature
- Common JWT security mistakes
- HS256 vs RS256
- JWT decoder read claims without a key
- HMAC-SHA256 the HS256 primitive