Skip to main content
{/}codefunc
Web & APIJuly 8, 2026 · 4 min read

How to Verify a JWT Signature (Without Leaking Secrets)

Decoding a JWT is not verifying it. Learn how signature verification works, the alg:none and algorithm-confusion attacks to avoid, which claims to check, and how to inspect tokens safely in development.

By codefunc

jwtsecurityauthenticationapioauth

A decoded JWT looks trustworthy — clean JSON with a user id, roles, and an expiry. But decoding proves nothing. A forged token decodes to valid JSON just as easily as a real one. Only signature verification proves a token was issued by a party holding the signing key and has not been tampered with. Getting this step wrong is one of the most common — and most severe — authentication bugs.

Bottom line: Always verify the signature server-side before trusting any claim. Pin the expected algorithm, use the correct key type, and check exp, nbf, iss, and aud. Decoding is for reading; verification is for trusting.

Decoding vs verifying

A JWT is three Base64url segments: header.payload.signature. Decoding the first two is trivial and requires no key:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.<signature>
└─ header ─┘         └─ payload ─┘        └─ proof ─┘

The signature is computed over header.payload using a secret (HMAC) or a private key (RSA/ECDSA). Verification recomputes or checks that signature. If it does not match, the token is rejected — full stop.

Decoding answers "what does this claim?". Verification answers "can I trust it?".

How the signature works

There are two families:

Family Algorithms Sign with Verify with
Symmetric (HMAC) HS256, HS384, HS512 shared secret the same shared secret
Asymmetric RS256, ES256, PS256 private key public key

With HMAC, the same secret both signs and verifies — so every service that verifies can also forge. With asymmetric algorithms, only the holder of the private key can sign; anyone can verify with the public key. For third parties (OAuth providers, SSO), asymmetric is the norm.

The verification steps

A correct verifier does all of the following:

  1. Parse the header and confirm the alg is one you expect.
  2. Select the key — your HMAC secret, or the issuer's public key (often fetched from a JWKS endpoint by kid).
  3. Recompute the signature over header.payload and compare it to the token's signature using a constant-time comparison.
  4. Validate the claimsexp (not expired), nbf (active yet), iss (expected issuer), aud (intended for you).

Skipping any step opens a hole.

The attacks you must prevent

1. alg: none

The JWT spec includes a none algorithm meaning "unsigned". A naive library that honors it will accept a token with an empty signature. Never accept none. Always require a specific algorithm.

2. Algorithm confusion (RS256 → HS256)

If a server verifies with "whatever the header says", an attacker can take a public RS256 key (which is public), craft a token with alg: HS256, and sign it using the public key as an HMAC secret. The server, trusting the header, verifies with that same public key and accepts the forgery.

The fix is to pin the algorithm in your verification call, independent of the token header:

// Node, using jsonwebtoken — pin the algorithm
import jwt from "jsonwebtoken";

const decoded = jwt.verify(token, publicKeyOrSecret, {
  algorithms: ["RS256"],   // never let the token choose
  issuer: "https://auth.example.com",
  audience: "my-api",
});

3. Trusting an unverified kid

If you pick the key from a kid header, only accept keys from a trusted JWKS you control — never fetch arbitrary URLs the token points to.

Checking claims, not just the signature

A valid signature on an expired token is still invalid for access. Enforce the temporal and scope claims:

const now = Math.floor(Date.now() / 1000);
if (payload.exp && now >= payload.exp) throw new Error("expired");
if (payload.nbf && now < payload.nbf) throw new Error("not yet valid");
if (payload.iss !== EXPECTED_ISSUER) throw new Error("bad issuer");
if (!toArray(payload.aud).includes(MY_AUDIENCE)) throw new Error("wrong audience");

Libraries do this when you pass issuer/audience options — use them rather than hand-rolling.

Where a browser tool fits

Server code must be the source of truth. A browser-based tool is for development and debugging: confirming a token was signed with the secret you think it was, checking why a token is rejected, or inspecting expiry during an auth bug hunt.

  • Use the JWT Verifier to check an HS256/384/512 signature and expiry locally — the token and secret never leave your browser.
  • Use the JWT Decoder to read claims without a key.
  • Use the JWT Generator to mint test tokens with a known secret.

Prefer expired or throwaway test tokens, and never paste a production signing secret into any browser tool.

Common mistakes

  • Verifying with the algorithm from the token header instead of a pinned list.
  • Accepting alg: none.
  • Checking the signature but forgetting exp/aud.
  • Comparing signatures with == instead of a constant-time compare (timing side channel).
  • Putting sensitive data in the payload — remember it is only encoded, not encrypted.

Practical takeaway

Treat decode and verify as different operations. On the server, pin the algorithm, use the right key type, reject none, and validate exp/nbf/iss/aud on every request. Use browser tools only to debug with test credentials, and keep production secrets out of them.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool