JWT invalid signature — Secret, Algorithm, and Key Rotation
Diagnose JWT invalid signature errors by decoding the header, verifying with the correct algorithm and key, checking kid rotation, and reproducing the exact bytes.
Symptoms
- JsonWebTokenError: invalid signature
- JWT signature verification failed in the API gateway
- Tokens issued by one service are rejected by another
- Verification works locally but fails after a deployment or key rotation
Possible causes
- The verifier uses a different secret or public key than the issuer
- The token is signed with HS256 but verified as RS256, or the public key is used as an HMAC secret
- Key rotation introduced a new kid that the verifier cannot resolve
- The token was truncated by a header size limit or modified after signing
Diagnosis steps
Decode the header without verifying
node -e "const t=process.argv[1]; const [h]=t.split('.'); console.log(JSON.parse(Buffer.from(h,'base64url')));" "$TOKEN"Interpretation
The alg and kid fields tell you which algorithm and key the issuer used. A missing kid makes rotation ambiguous; an unexpected alg explains a mismatch.
Verify with the expected algorithm explicitly
node -e "const jwt=require('jsonwebtoken'); console.log(jwt.verify(process.argv[1], process.env.JWT_SECRET, { algorithms: ['HS256'] }));" "$TOKEN"Interpretation
Success proves the secret and algorithm match. Failure with a valid token points to a different secret, a different signing input, or a malformed token.
Reproduce RS256 verification independently
node -e "const fs=require('fs'); const [h,p,s]=process.argv[1].split('.'); fs.writeFileSync('signing.txt', h+'.'+p); fs.writeFileSync('sig.bin', Buffer.from(s,'base64url'));" "$TOKEN" && openssl dgst -sha256 -verify public.pem -signature sig.bin signing.txtInterpretation
A Verified OK result means the public key and signature match and the problem is in the verifier configuration, not the token.
Check key and secret configuration across services
env | grep -iE 'JWT|JWKS' | sed 's/=.*/=<set>/'Interpretation
Compare which key source each service uses. Presence of a secret is not enough; the values must match, and an empty or placeholder value produces invalid signature errors.
Root cause
A JWT signature is computed over the exact base64url header and payload with a specific algorithm and key. Verification fails if any byte differs, if the key does not correspond to the issuer's, or if the verifier chooses a different algorithm. Rotation adds a second failure mode when the token's kid refers to a key the verifier has not loaded.
Fix
Use the same key material and algorithm on issuer and verifier, enforce an allowlist of algorithms instead of trusting the header, and resolve keys by kid from a JWKS endpoint that is refreshed. Compare secrets through a secure channel or configuration management, not by copying from logs. Do not decode without verification or ignore expiration to work around the error.
Risk note
Accepting the alg value from the token enables algorithm-confusion attacks, including verifying an RS256 token as HS256 with the public key. Disabling signature checks or expiration makes the API accept forged tokens. Logging full tokens exposes bearer credentials.
Verify the fix
Verify the token with the production key and a strict algorithm allowlist and confirm it succeeds, then verify a token signed with a different key and confirm it fails. Test a rotated token with a new kid and confirm the verifier refreshes JWKS and accepts it.
The signature covers exact bytes
A JWT has three base64url segments. The signature is computed over header.payload exactly as transmitted. Any change to either segment, or a different key, produces:
JsonWebTokenError: invalid signature
Decode the header first
node -e "const [h]=process.argv[1].split('.'); console.log(JSON.parse(Buffer.from(h,'base64url')));" "$TOKEN"
Check alg and kid. If the issuer uses RS256 and your verifier assumes HS256, the error is expected. If kid is present, the verifier must load that key.
Verify independently
node -e "const jwt=require('jsonwebtoken'); console.log(jwt.verify(process.argv[1], process.env.JWT_SECRET, { algorithms: ['HS256'] }));" "$TOKEN"
For RS256, split the token and use OpenSSL:
node -e "const fs=require('fs'); const [h,p,s]=process.argv[1].split('.');
fs.writeFileSync('signing.txt', h+'.'+p);
fs.writeFileSync('sig.bin', Buffer.from(s,'base64url'));" "$TOKEN"
openssl dgst -sha256 -verify public.pem -signature sig.bin signing.txt
Verified OK isolates the token and key from the application’s verifier.
Fix configuration, not validation
- Share the same secret or public key through configuration management.
- Enforce
algorithms: ['RS256']or the exact expected value; never trust the header. - Resolve
kidfrom JWKS and refresh on unknown key IDs. - Check for truncation by proxies and header size limits.
- Keep clock skew tolerance small and do not disable expiration.
Verify
Verify a token from the issuer with the production key and strict algorithms; it should succeed. Verify a token signed with a different key; it must fail. Rotate a key, issue a token with a new kid, and confirm the verifier fetches it and accepts the token without accepting the old algorithm.