Error message
invalid signature
What it means
The token parsed fine and its structure is valid, but the cryptographic check failed: the signature over header.payload does not verify against the key you supplied. Either the key is not the one that signed the token, or the signed bytes changed after signing.
Why it happens
Verifying with the wrong key
commonThe most frequent case by far: the environment holds a different key than the issuer used — a stale public key after rotation, a staging key against production tokens, or a JWKS kid mismatch when the verifier pins one key while the issuer signs with another.
PEM mangled by environment variables
commonA multi-line PEM stored in .env or a secret manager often arrives with literal \n two-character sequences instead of newlines, or with surrounding quotes. Depending on the Node version this either fails to load or loads a subtly wrong key that verifies nothing.
The token was modified after signing
occasionalA proxy, logger or client re-encoded the payload (base64 padding, whitespace, re-serialized JSON) or the token was truncated. Even one changed character in header or payload invalidates the signature — that is the whole point of signing.
Cross-library ES256 format mismatch
occasionalIf the token was minted by hand-rolled code, the ECDSA signature may be ASN.1 DER (~70 bytes) instead of the raw 64-byte r‖s that RFC 7518 requires for ES256. Standards-compliant verifiers reject DER-shaped JWS signatures.
How to fix it
- 1.
Fix escaped newlines in the key
If the key comes from an environment variable, normalize it before use, and print the first line to make sure it is the PEM you expect.
js const pem = process.env.JWT_PUBLIC_KEY.replace(/\\n/g, "\n"); console.log(pem.split("\n")[0]); // -----BEGIN PUBLIC KEY----- jwt.verify(token, pem, { algorithms: ["ES256"] }); - 2.
Match the token's kid to the verifying key
Decode the header without verifying and compare its kid with the key you hold. If the issuer publishes a JWKS, resolve the key by kid instead of hard-coding one.
js const { header } = jwt.decode(token, { complete: true }); console.log(header.kid, header.alg); // compare with your JWKS / configured key - 3.
Let the debugger name the mismatch
Paste the token and public key into the JWT debugger: it verifies ES256/ES384/ES512 locally and distinguishes wrong key, tampered payload and malformed signature encoding instead of a bare boolean.