JWT Guides

How I Debug a JWT Invalid Signature Error

See the exact order I use to troubleshoot JWT invalid signature errors caused by wrong keys, algorithm mismatches, key rotation, encoding problems, or modified token bytes.

By JWTDecoder.toolsLast verified 2026-09-16

Editorial voice: first-person phrasing describes the documented JWTDecoder.tools workflow; JWTDecoder.tools is the publisher and organizational author.

Decision flow for debugging a JWT invalid signature error
Confirm algorithm, issuer, kid and key selection before changing verification settings.

When I see invalid signature, JWT signature verification failed, or JWSSignatureVerificationFailed, I do not start by editing the token or disabling checks. I preserve the exact compact JWT and split the investigation into four inputs: signed bytes, algorithm policy, key material, and key selection.

If I already have trusted HMAC or RSA key material, I reproduce the cryptographic check in the JWT Signature Verifier. If verification depends on a key set, I use the JWKS Validator.

What an invalid signature error proves — and what it does not

A compact JWS signs this input:

base64url(header) + "." + base64url(payload)

The verifier recomputes or checks a signature using trusted key material. Failure means those cryptographic inputs do not line up.

It does not by itself mean:

  • the token is expired;
  • aud is wrong;
  • the user lacks a role;
  • the JSON is malformed;
  • the JWT library is broken.

Those are separate layers.

My first branch: HMAC or public-key signature?

I decode the header only as a diagnostic clue:

{ "alg": "HS256" }

or:

{ "alg": "RS256", "kid": "prod-2026-09" }

Then I branch:

HS256 / HS384 / HS512
  -> secret bytes, encoding, secret rotation

RS256 / RS384 / RS512
  -> public key, PEM/JWK, kid, JWKS, issuer

ES*/PS*
  -> public key + algorithm/key compatibility + implementation support

The allowed algorithm set comes from application configuration. I never make the token header the policy source.

Exact error strings tell me which phase failed

I treat these differently:

JsonWebTokenError: invalid signature
JWSSignatureVerificationFailed
signature verification failed
InvalidSignatureError
kid not found
no applicable key found in the JSON Web Key Set
jwt malformed

jwt malformed fails before cryptography. kid not found is usually key selection. invalid signature normally means a candidate key was actually used and did not verify. That phase distinction prevents me from chasing secret encoding when the real problem is an empty JWKS lookup.

HMAC: I compare key bytes, not screenshots of configuration strings

For HS256, two systems can display the same-looking value while using different bytes.

I check:

  • UTF-8 text vs Base64-decoded secret;
  • trailing newline from a secret file or environment variable;
  • quotes accidentally included in the secret;
  • different secrets in dev/staging/prod;
  • secret rotation;
  • secrets that are too weak for the algorithm and threat model.

Example:

const rawUtf8 = new TextEncoder().encode(process.env.JWT_SECRET);
const decodedBase64 = Uint8Array.from(
  atob(process.env.JWT_SECRET),
  char => char.charCodeAt(0)
);

Those are different keys. The correct interpretation is whatever the signer and verifier contract specifies.

RSA: I distinguish SPKI, PKCS#1, certificate, and JWK

Common inputs include:

-----BEGIN PUBLIC KEY-----       # usually SPKI
-----BEGIN RSA PUBLIC KEY-----   # PKCS#1 public key
-----BEGIN CERTIFICATE-----      # X.509 certificate containing a public key

Some libraries accept several forms; others expect a specific one. I convert deliberately rather than stripping PEM headers until a parser stops complaining.

If I have a JWK, I also inspect kty, alg, use, and key_ops when present.

kid not found is a JWKS/cache problem until proven otherwise

When a token carries:

{ "alg": "RS256", "kid": "key-2026-09" }

I look in the trusted issuer's current JWKS. Common causes of a miss are:

  • verifier cache is stale;
  • issuer has begun signing with a new key before verifiers saw it;
  • old key was retired while old tokens remain active;
  • wrong tenant or issuer environment;
  • duplicate/ambiguous kid handling;
  • matching kid exists but kty/alg is incompatible.

I do not fetch an arbitrary key URL from untrusted token data and automatically trust it.

Re-serializing JSON can destroy a valid signature comparison

These payloads are semantically similar:

{"sub":"42","role":"user"}
{ "role": "user", "sub": "42" }

but they produce different serialized bytes. I always verify the original compact header.payload segments, not JSON I decoded and re-encoded.

This is also why manually changing exp, aud, or role necessarily changes the signed input.

A practical Node diagnostic with jsonwebtoken

With trusted local key material, a minimal reproduction can isolate the library/configuration layer:

import jwt from "jsonwebtoken";

try {
  const payload = jwt.verify(token, publicKey, {
    algorithms: ["RS256"],
    issuer: "https://login.example.com/",
    audience: "api://orders"
  });
  console.log(payload);
} catch (error) {
  console.error(error.name, error.message);
}

For an invalid signature test, I may temporarily remove issuer/audience options only in a local diagnostic to determine whether cryptography fails before claims. I do not deploy weaker validation as a fix.

A jose reproduction makes the key-selection boundary explicit

import { jwtVerify } from "jose";

const result = await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://login.example.com/",
  audience: "api://orders"
});

If the application normally uses remote JWKS, I reproduce with the same trusted issuer/JWKS relationship rather than a random public key that happens to verify.

How key rotation changes the diagnosis

Rotation introduces a timeline:

old tokens -> kid=K1
new JWKS   -> K1 + K2
new tokens -> kid=K2
later      -> K1 removed after policy allows

If K2 tokens fail, I ask whether K2 was published before signing started and whether unknown-kid triggers a controlled refresh. If K1 tokens fail, I ask whether K1 was removed before those tokens aged out.

The JWT key rotation guide covers cache propagation and overlap in detail.

Decision matrix: what I test next

SymptomMost likely layerFirst check
HS256 invalid signaturesecret bytesraw vs Base64, newline, environment
RS256 invalid signaturepublic keyissuer-bound PEM/JWK
kid not foundselection/cachecurrent trusted JWKS
algorithm mismatchpolicyconfigured allowlist
failure only after rotationlifecycle/cacheK1/K2 overlap, refresh
failure after editing tokensigned bytesoriginal compact JWT
signature passes, API still 401claimsexp/iss/aud/type

Security shortcuts I explicitly reject

  • disable signature verification;
  • accept whatever alg appears in the header;
  • switch RS256/HS256 dynamically because one happens to pass;
  • trust a token-provided key URL automatically;
  • try every unrelated key until one verifies;
  • weaken an HMAC secret for convenience;
  • treat successful decoding as successful verification.

RFC 8725 is clear about constraining algorithms and key use rather than turning attacker-controlled metadata into policy.

My deterministic invalid-signature flow

  1. Preserve the original compact JWT.
  2. Decode alg, kid, iss, and aud with the JWT Decoder.
  3. Confirm the application's allowed algorithms.
  4. Identify the expected issuer and its trusted key source.
  5. For HMAC, compare exact secret bytes and encoding.
  6. For RSA/EC, verify key format, key type, and provenance.
  7. For JWKS, check kid, cache age, rotation, and ambiguity.
  8. Verify the original header.payload bytes.
  9. If cryptography succeeds, validate exp, nbf, iss, aud, and token type with the JWT Validator.
  10. If the API still rejects, use JWT Debugger to separate authentication from authorization.

An invalid signature is not a cosmetic parsing problem. It is evidence that the verifier cannot establish integrity/authenticity with the key path it was configured to trust. The fastest fix is to identify which input to verification differs, not to bypass the check.

Sources

Related guides