Fix the JWT Invalid Signature Error

When verify() throws invalid signature, the token itself is almost never corrupt. The failure means the third segment of the JWT (the signature) does not match what your verifier recomputes from the header, payload, and key it was given. That is a key or algorithm problem, not a token problem. It is a different fault from an expired token, where the signature is valid but the exp claim is in the past, which we cover separately in our JWT expired token fix guide. Below is a deterministic triage order that isolates the cause in minutes instead of guessing.

Step 1: Decode the header and read the alg

A JWT is three base64url segments joined by dots: header.payload.signature. The first thing your verifier reads is the alg field in the header, and it must agree with the key type you pass in. Before touching code, paste the raw token into the JWT decoder and read the header. Decoding inspects the token without trusting the signature, so it works even on a token that is currently failing verification.

Confirm two things. First, what does alg actually say: HS256, RS256, ES256, or something else? Second, does that match what your verify call expects? Per RFC 7515, the alg header tells the verifier which algorithm to use, but a secure verifier should pin an explicit allowlist rather than trust the token's claim. A mismatch between the token's algorithm family and your key is the root of most invalid-signature errors.

Step 2: HS256 secret mismatch across environments or after rotation

If alg is HS256 (or any HS variant), the signature is an HMAC keyed by a shared secret. HMAC verification only succeeds if the verifier uses the byte-for-byte identical secret that signed the token. The most common cause of invalid signature is that the secret differs between where the token was minted and where it is checked.

  • Environment drift: the token was issued in staging but verified in dev, or production rotated its secret while a service still holds the old one. A token signed with secret A will never verify against secret B.
  • Rotation timing: right after a secret rotation, in-flight tokens signed with the previous key all fail. Either keep the old key in a verification keyset during a grace window, or expect a wave of failures until old tokens expire.
  • Trailing characters: a secret loaded from a .env file or a secrets manager can pick up a trailing newline or quote. "mysecret\n" is a different key from "mysecret".

Step 3: RS256 vs HS256 confusion

This is the trap that produces the most confusing invalid-signature errors. HS256 and RS256 use completely different key material. HS256 is symmetric: one shared secret string both signs and verifies. RS256 is asymmetric: an RSA private key signs, and the matching public key verifies. If your token is RS256 but your verify call is handed an HMAC secret string, verification cannot succeed, and the reverse fails too. For the full breakdown of how the families differ, see our JWT signing algorithms comparison.

The tell is in Step 1. If the decoder shows RS256, your verifier needs the RSA public key (usually a PEM block beginning -----BEGIN PUBLIC KEY-----), not a password-like string. If it shows HS256, you need the shared secret, and passing a PEM public key will fail. A frequent variant: a service was migrated from HS256 to RS256 but one consumer still passes the old shared secret.

Step 4: Base64 or whitespace damage to the key

Even with the right algorithm and the right logical secret, the key bytes can arrive mangled. Watch for these specific corruptions:

  • Double-encoding: a secret stored as base64 that your code decodes once when it should not, or signs raw while verifying decoded. The signer and verifier must treat the secret bytes identically.
  • Whitespace and newlines: copy-paste, YAML block scalars, and editors can inject spaces, tabs, or \r\n line endings. Trim and normalize before use.
  • PEM line breaks: an RSA key flattened into a single-line environment variable loses the newlines a PEM parser expects. The literal \n sequences must be converted back to real newlines.
  • Encoding mismatch: the secret read as UTF-8 in one service and as a different encoding in another yields different bytes.

Step 5: decode() called instead of verify()

Sometimes there is no real signature failure at all. Many libraries expose a decode() that reads claims without checking the signature, and a separate verify() that checks it. If a test passed with decode() and then someone switched to verify(), the error surfaces only now because decoding never checked the key in the first place. Confirm you are calling the verifying function with the correct key argument, and that you are not accidentally verifying a re-serialized token. Re-encoding the payload (for example, reordering JSON keys) changes the signing input and breaks the signature even when the key is correct.

The isolation trick: verify with a key you control

When the steps above do not pinpoint the cause, remove every variable by testing your key against the token directly, outside your application code. This separates a bad key from a bad verify path.

  1. Paste the failing token into the JWT decoder, enter your secret (for HS256) or your PEM public key (for RS256) in the key field, and click Verify Signature. If the decoder reports the signature as valid with that exact key, your key is correct and the bug is in how your application loads or passes it, sending you back to Step 4. If the decoder also rejects it, the key or algorithm is genuinely wrong, sending you back to Step 2 or Step 3.
  2. For HS256, recompute the signature by hand. Take the header.payload portion of the token (everything before the last dot) and feed it to the HMAC generator with SHA-256 and your secret. Base64url-encode the result and compare it to the token's third segment.
  3. If your hand-computed value matches the token's signature but your library still fails, the difference is in key loading or encoding inside your code, not in the token. If it does not match, your secret is wrong.

This works because HS256 signs the exact ASCII string BASE64URL(header) + "." + BASE64URL(payload), so an HMAC-SHA256 of that string with the correct secret reproduces the signature byte for byte. Any deviation, in the secret, the encoding, or the bytes being signed, changes the output.

Quick reference checklist

SymptomLikely causeAction
alg is RS256, you pass a stringAlgorithm family mismatchSupply the RSA public key
Worked in dev, fails in prodSecret drift or rotationCompare secrets byte for byte
Fails only after a deployRotated key, old tokens in flightKeep old key in a verify keyset
Secret looks right but failsWhitespace or double-encodingTrim and normalize the key bytes
Never checked beforedecode() used, not verify()Call the verifying function

Frequently Asked Questions

It means the signature segment of the token does not match what your verifier recomputes from the header, payload, and key it was given. The token is structurally fine; the verifier is using the wrong secret, the wrong key type, or the wrong algorithm. It is not the same as an expired or malformed token.

Almost always a secret mismatch. HS256 verification requires the exact same shared secret that signed the token. If dev, staging, and production hold different secrets, or one service kept an old secret after rotation, tokens signed elsewhere will fail with invalid signature even though they are otherwise valid.

Yes. HS256 uses a shared secret string while RS256 uses an RSA key pair. If a token is signed with RS256 but you hand the verifier an HMAC secret, or the reverse, verification cannot succeed. Decode the header first and confirm the alg matches the key material you are passing in.

Test the key outside your application. Paste the failing token into a JWT decoder, enter the same secret or public key, and verify the signature there. If it passes, your key is correct and the bug is in how your code loads or passes it. If it also fails there, the key or algorithm is genuinely wrong.

decode() reads the claims without checking the signature, so it never touches your key. verify() actually validates the signature against the key. Switching from decode to verify exposes a key or algorithm problem that was always present but never tested. Make sure you pass the correct key to the verifying function.