Common causes
- The signing and verifying services use different secrets, for example different .env values per environment
- The secret has a trailing newline, quotes or spaces from the environment file
- The secret is base64-encoded in one place and used as raw text in the other
- An RS256/ES256 token is verified with the wrong public key or a rotated key (kid mismatch in JWKS)
- The token was copied with extra characters or edited, so its bytes no longer match the signature
- The token was issued by a different identity provider, tenant or project than the one you verify against
How to fix it
- Decode the header and payload. Decode the token without verifying and look at alg, kid and iss. They tell you which algorithm and key should be used and who issued the token.
- Compare secrets exactly. Print the length and a hash of the secret in both services, for example node -e "console.log(require('crypto').createHash('sha256').update(process.env.JWT_SECRET).digest('hex'))". Different hashes mean different secrets.
- Check encoding. If the secret was generated as base64, decode it to bytes in both places (Buffer.from(secret, 'base64') in Node) or use it as text in both places, but not mixed.
- Use the right key for asymmetric algorithms. For RS256 or ES256, verify with the issuer's public key in PEM format or fetch it from the JWKS endpoint by kid. Refresh cached keys after the provider rotates them.
- Pin the algorithm. Pass an explicit allow-list such as algorithms: ['HS256'] when verifying. This prevents algorithm confusion attacks and makes mismatches fail with a clear error.
- Issue a fresh token. After changing secrets, existing tokens signed with the old key will keep failing. Log in again or refresh the token to get one signed with the current key.
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET.trim();
const token = jwt.sign({ sub: '42' }, secret, { algorithm: 'HS256', expiresIn: '1h' });
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] }); How to stop it happening again
- Load the signing secret from one shared secret store, not hand-copied .env files
- Always pin accepted algorithms when verifying
- Use kid and JWKS with key rotation for asymmetric keys
- Validate environment files for missing or malformed variables during deploy