Error message
JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
Also printed as
- io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
JJWT (Java) — io.jsonwebtoken.security.SignatureException from JwtParser.parseSignedClaims
What it means
JJWT recomputed (HMAC) or verified (RSA, ECDSA, EdDSA) the signature over the token's header and payload with the key you configured, and it did not match the token's signature segment. The token parsed fine and the algorithm fit the key; the key and the token simply do not belong together, or the token changed after signing.
Why it happens
The verifier uses a different key
commonAnother environment's key, a key pair regenerated on startup (a common bug in demos promoted to production), or one service instance with an outdated secret.
Same HMAC secret, different bytes
commonOne side uses the string's UTF-8 bytes, the other base64-decodes it first (Spring's examples often store base64 secrets). The secrets look identical in configuration and produce different MACs.
Key rotation without kid handling
occasionalThe issuer signs with a new key while the verifier still has only the old one. Without a key locator keyed by `kid`, the verifier cannot even tell which key was meant.
The token was altered
occasionalAny change to the header or payload segment — including whitespace or re-encoding — invalidates the signature.
How to fix it
- 1.
Resolve keys by kid with a key locator
JJWT 0.12+ supports `keyLocator`, which receives the header and returns the right key. Combine it with a JWKS that you refresh on rotation.
java import io.jsonwebtoken.*; import java.security.Key; // Pick the verification key by the token's kid instead of a single static key. Locator<Key> keys = new LocatorAdapter<>() { @Override protected Key locate(JwsHeader header) { Key key = publicKeysByKid.get(header.getKeyId()); // refreshed from the issuer's JWKS if (key == null) throw new UnsupportedJwtException("unknown kid " + header.getKeyId()); return key; } }; Jws<Claims> jws = Jwts.parser().keyLocator(keys).requireIssuer("https://issuer.example.com/").build().parseSignedClaims(token); - 2.
Agree on the HMAC key bytes
Decide whether the configured secret is text or base64 and derive the key the same way on every side. The HMAC calculator shows the MAC for both interpretations.
java // Both sides must derive the same key bytes. If the secret is stored base64-encoded, decode it. javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor( java.util.Base64.getDecoder().decode(System.getenv("JWT_SECRET_B64")));