Common causes
1. Issuer and verifier use different secrets
An HS256 token signed with the staging secret fails against the production secret, and a secret with a trailing space or newline from a copied .env value is a different secret too. Compare the values byte for byte.
# api-service/.env (auth-service signs with production-secret-2026)
JWT_SECRET=staging-secret-2026# api-service/.env (auth-service signs with production-secret-2026)
JWT_SECRET=production-secret-20262. A secret that is Base64-encoded on one side only
Some issuers (Auth0 legacy apps, Java’s jjwt with Base64 keys) treat the secret as Base64 and sign with the decoded bytes. Verify with the same bytes.
jwt.verify(token, process.env.JWT_SECRET);jwt.verify(token, Buffer.from(process.env.JWT_SECRET, 'base64'));3. The wrong public key for RS256 or ES256
With asymmetric algorithms you verify with the issuer’s public key, and identity providers rotate keys. Select the key by the token’s kid from the provider’s JWKS endpoint instead of hard-coding one.
const { payload } = await jwtVerify(token, await importSPKI(OLD_PUBLIC_KEY_PEM, 'RS256'));const jwks = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, jwks, { issuer: 'https://auth.example.com/' });4. A token changed after it was signed
Editing the payload (for example on a debugging site), re-encoding it, or keeping the "Bearer " prefix changes the signed bytes. Strip the prefix and pass the token exactly as issued.
const token = req.headers.authorization;const token = req.headers.authorization?.replace(/^Bearer\s+/i, '');Frequently asked questions
Can I read a JWT whose signature is invalid?
Yes. The header and payload are only Base64URL-encoded, so anyone can decode them. That is exactly why the payload must never be trusted until the signature has been verified.
Is it safe to paste a token into PasteKit to check it?
Decoding and verification run entirely in your browser and nothing is uploaded. Still, treat live tokens as credentials and prefer expired or test tokens when sharing screenshots.
Why does jwt.io say "Signature Verified" when my server rejects it?
Usually the secret or key differs between the two places: check for encoding (plain vs Base64), whitespace and environment. Also make sure the server allows the token’s algorithm.