Debugging JSON Web Token issues can be frustrating. Cryptographic operations fail silently or throw cryptic errors like invalid signature without indicating whether the issue is an encoding bug, an incorrect secret, or clock drift.
This guide breaks down the 6 most common JWT errors, explaining the root cause behind each failure and providing exact code fixes.
1. Error: "JsonWebTokenError: invalid signature"
This is the most frequent error encountered in JWT verification. It means the signature computed by the server does not match the signature provided in the third part of the token.
🛠️ JWT Error Diagnostic & Troubleshooting Matrix
Diagnostic FlowCommon Causes & Solutions:
-
Raw String vs. Base64 Encoded Secret: If your auth provider generated a Base64-encoded secret, but your code passed it as a raw UTF-8 string, the computed HMAC will not match.
Fix: Decode Base64 Secret First
// WRONG: passes Base64 string directly as UTF-8 jwt.verify(token, "c2VjcmV0"); // CORRECT: decodes Base64 string into a Buffer jwt.verify(token, Buffer.from("c2VjcmV0", "base64")); -
Trailing Whitespace or Newlines: Copy-pasting secrets or private keys from
.envfiles often introduces invisible newline characters (\n). Always run.trim()on your secret strings before verification. -
Using Public Key as HMAC Secret: If the token was signed with RS256, verifying it with
jwt.verify(token, secret)expecting HMAC will fail.
2. Error: "TokenExpiredError: jwt expired"
The current time is greater than the timestamp recorded in the token's exp (expiration) claim.
Common Causes & Solutions:
-
Milliseconds vs. Seconds Confusion: The JWT specification (RFC 7519 NumericDate) strictly requires dates in seconds since Unix epoch. If you pass JavaScript's
Date.now()(which is in milliseconds), your token will be valid for thousands of years or fail immediately due to overflow parsing.Fix: Convert Milliseconds to Seconds// WRONG: Date.now() returns milliseconds (e.g. 1729420000000) const payload = { exp: Date.now() + 3600 }; // CORRECT: Divide by 1000 and floor const payload = { exp: Math.floor(Date.now() / 1000) + 3600 }; -
Server Clock Skew: If the authorization server and API gateway have clocks that differ by even 2 seconds, tokens may fail with
jwt expiredorjwt not active yetimmediately upon issuance. ConfigureclockTolerance:Fix: Add Clock Tolerance Windowjwt.verify(token, publicKey, { clockTolerance: 60 // Allow 60 seconds of clock drift });
3. Error: "JsonWebTokenError: jwt malformed"
The token structure does not conform to the required header.payload.signature tripartite layout.
Common Causes & Solutions:
-
Forgetting to Strip "Bearer ": Passing the entire
Authorizationheader string into the verification function is the #1 cause of malformed token errors.Fix: Safe Header Parsingconst authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing or invalid authorization header" }); } const token = authHeader.split(" ")[1].trim(); jwt.verify(token, secret); - Missing Dot Separators or Double URL Encoding: Ensure the token has exactly two dots dividing three segments.
4. Error: "JsonWebTokenError: invalid algorithm"
The verification library was instructed to allow only specific algorithms, and the incoming token's header specifies a different or unauthorized algorithm.
// If token was signed with RS256:
jwt.verify(token, publicKey, {
algorithms: ["RS256"] // Make sure token header also specifies "alg": "RS256"
});
5. Error: "jwt audience invalid" / "jwt issuer invalid"
The signature was valid, but claim validation failed.
Common Causes & Solutions:
-
Trailing Slash Discrepancies in Issuer:
https://auth.company.comdoes NOT equalhttps://auth.company.com/. Ensure the issuer string matches character for character. -
Array vs. String Audience: If your token has multiple audiences:
"aud": ["https://api.v1", "https://api.v2"], your verification library must support checking array membership.
6. Base64 vs. Base64URL Encoding Issues
Standard Base64 uses characters + and /, and pads with =. JWTs require Base64URL encoding, which replaces + with -, / with _, and removes all padding = characters. If you decode or encode manually with naive atob() or btoa(), URL characters will be corrupted.