Error message
signature verification failed
jose (JavaScript) — JWSSignatureVerificationFailed, code ERR_JWS_SIGNATURE_VERIFICATION_FAILED
What it means
jose parsed the JWS successfully and ran the cryptographic check, but the signature does not verify against the key it selected. Structure, algorithm and key type were all acceptable — the mathematics simply did not match, which points at the key or at altered signed bytes.
Why it happens
Wrong key for this token
commonThe verifier holds a different key than the issuer signed with: a rotated JWKS where the old kid is gone, a hard-coded key that fell out of date, or an environment mix-up (staging verifier, production token).
Signed bytes changed between signer and verifier
commonJWS signs the exact base64url text of header.payload. Anything that re-encodes those segments — a gateway normalizing base64 padding, JSON re-serialization, trimmed characters in transport — breaks verification even though the decoded payload looks identical.
Hand-rolled ES256 signer produced DER
occasionalNode's crypto.sign() emits ASN.1 DER for ECDSA by default, but RFC 7518 requires ES256 JWS signatures to be raw r‖s (exactly 64 bytes). A token whose third segment decodes to ~70–72 bytes starting with 0x30 was signed in the wrong format.
How to fix it
- 1.
Resolve keys by kid from the issuer's JWKS
Let jose pick the right key per token instead of pinning one manually; createRemoteJWKSet caches and refreshes keys across rotations.
js import { createRemoteJWKSet, jwtVerify } from "jose"; const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example", }); - 2.
If you sign with node:crypto, emit raw r‖s
For custom signers, request IEEE P1363 output so the signature is the 64-byte format JWS requires — or verify what your signer produced by checking the third segment's length.
js import { sign } from "node:crypto"; const sig = sign("sha256", Buffer.from(signingInput), { key: privateKey, dsaEncoding: "ieee-p1363", // raw r||s, 64 bytes for P-256 }); - 3.
Check the signature segment's shape
Decode the token's third segment: 64 bytes means raw r‖s (correct for ES256); ~70 bytes starting with 0x30 means DER. The DER ⇄ raw converter below shows the r and s values and converts between the two formats.