← Debugging Guides

阅读中文版本

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.

intermediatedebuggingjwtauthentication

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.txt

Interpretation

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 kid from 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.