You paste a JWT into your debugger, hit verify, and get: Invalid signature. The payload looks fine. The header looks fine. But verification fails โ and now you are stuck wondering whether the token, the secret, or your code is wrong.
This guide walks through every common cause in order, with concrete fixes. Use our JWT Decoder to inspect tokens locally โ your secrets never leave the browser.
How JWT signature verification works
A JWT signature is computed over the Base64URL-encoded header and payload, joined by a dot, using the algorithm declared in the header (for example HS256 or RS256). Verification recomputes that signature and compares it to the third segment of the token. If they differ, you get an invalid signature error.
signed_content = base64url(header) + "." + base64url(payload) signature = HMAC-SHA256(signed_content, secret) // for HS256
Cause 1: Wrong secret or key
The most common cause by far. The server signed the token with one secret, but your verifier is using a different one โ or an old secret after a rotation.
- Compare the exact secret byte-for-byte (no trailing newline from a
.envfile). - Check staging vs production environment variables.
- For RS256, ensure you are using the public key to verify, not the private key.
- Confirm the key format (PEM vs raw string vs base64-encoded).
Cause 2: Algorithm mismatch
The token header says HS256 but your library is verifying with RS256, or vice versa. Worse: some libraries trust the algorithm in the token header unless you explicitly pin it.
// Bad โ trusts whatever alg the token claims
jwt.verify(token, secret)
// Good โ pin the expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })Always pin the algorithm on the server. Never accept none.
Cause 3: Token was modified
Because the payload is only Base64-encoded (not encrypted), anyone can decode it, change a claim like role: "admin", re-encode it, and re-submit it. Without a valid new signature, verification fails โ which is exactly what should happen.
If you changed the payload yourself while debugging, you must re-sign it with the correct secret to produce a valid token.
Cause 4: Encoding and whitespace issues
Copy-pasting tokens often introduces problems:
- Leading or trailing whitespace or quotes around the token string.
- Line breaks in the middle of a token pasted from a log file.
- Using standard Base64 decode instead of Base64URL (JWT uses URL-safe encoding).
- Truncated token โ missing characters from the signature segment.
Cause 5: Expired or not-yet-valid token
Strictly speaking, expired tokens often return a different error (TokenExpiredError), but some frameworks lump all verification failures together. Check the exp and nbf claims in the payload. Clock skew between servers can also cause surprising failures โ allow a small leeway (for example 30 seconds) in production verifiers.
Step-by-step debugging checklist
- Decode the token (without verifying) and inspect header + payload.
- Confirm the
algclaim matches what your server expects. - Verify you are using the correct secret or public key for that environment.
- Strip whitespace and ensure the full three-segment token is present.
- Pin the algorithm in your verify call.
- Check
expand server clock sync. - If all else fails, generate a fresh token from the issuing server and compare.
Frequently asked questions
Can I verify a JWT without the secret?
You can decode the header and payload without the secret, but you cannot verify the signature. The secret (or public key for asymmetric algorithms) is required to prove the token is authentic.
Does invalid signature mean the token was tampered with?
Not necessarily. It means the signature does not match โ which includes tampering, wrong secret, wrong algorithm, or a corrupted copy of the token.
Why does my token work on one server but not another?
Different secrets, different algorithm configuration, or different clock settings between environments are the usual explanation. Compare environment variables and library config side by side.