JWT errors are some of the most cryptic messages you'll see in web development. The three you'll hit most often are TokenExpiredError, invalid signature, and malformed token. The fix depends on which part of the token is wrong — the timestamps, the signing key, or the encoding itself. This guide walks through every common JWT error, explains why it happens, and shows you how to decode, diagnose, and fix it.
How JWTs Work (30-Second Recap)
A JSON Web Token is a compact string made up of three parts separated by dots: a header, a payload, and a signature. Each part is Base64URL-encoded.
header.payload.signature
// Decoded header
{
"alg": "HS256",
"typ": "JWT"
}
// Decoded payload
{
"sub": "user_123",
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"exp": 1710787200,
"iat": 1710700800,
"nbf": 1710700800
}The header declares the signing algorithm. The payload carries claims about the user and token metadata. The signature is created by signing the encoded header and payload with a secret key (for HMAC algorithms) or a private key (for RSA/ECDSA). When a server receives a JWT, it re-computes the signature and compares it to the one in the token. If anything is off — wrong key, tampered payload, expired timestamp — you get an error.
The 8 Most Common JWT Errors
1. TokenExpiredError — exp Claim in the Past
This is the single most common JWT error. The exp claim is a Unix timestamp that defines when the token becomes invalid. Once the current time passes that value, every library will reject the token.
TokenExpiredError: jwt expired
at /node_modules/jsonwebtoken/verify.js:152:21Fix: Refresh the token before it expires. Most auth flows use short-lived access tokens (15 minutes) paired with longer-lived refresh tokens (7 days). When the access token expires, silently exchange the refresh token for a new access token.
2. Invalid Signature — Wrong Secret or Key
The signature verification failed. This means the key used to verify the token does not match the key used to sign it, or the token was tampered with after signing.
JsonWebTokenError: invalid signatureCommon causes:
- The signing secret changed between token creation and verification (e.g., after a deployment that rotated keys)
- Using an HS256 secret to verify a token that was signed with RS256 (algorithm mismatch)
- Different environments (dev vs. production) using different secrets
- Trailing whitespace or newlines in the secret from environment variable loading
Fix: Confirm the exact same key is used for signing and verification. For RSA/ECDSA, verify you're signing with the private key and verifying with the matching public key. Check for encoding issues — a Base64-encoded secret needs to be decoded before use.
3. Malformed Token — Not Three Parts or Invalid Base64
A valid JWT must have exactly three dot-separated segments. If the token is truncated, has extra characters, or contains non-Base64URL characters, the parser will reject it immediately.
JsonWebTokenError: jwt malformedCommon causes:
- Sending the entire
Authorizationheader value including theBearerprefix to the verify function - Token was URL-encoded during transmission and not decoded
- Token was truncated by a proxy, cookie size limit, or log output
Fix: Strip the Bearer prefix before verification. Log the raw token length to check for truncation. Paste the token into our JWT Decoder to see if it parses correctly.
4. Algorithm "none" Vulnerability
The JWT spec allows alg: "none" for unsecured tokens. Attackers exploit this by forging a token with no signature and setting the algorithm to none. If your server accepts it, they can impersonate any user.
{
"alg": "none",
"typ": "JWT"
}
// Signature is empty — token ends with a trailing dot
// eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiJ9.Fix: Always specify the allowed algorithms explicitly in your verification config. Never let the token header dictate which algorithm to use. Every major JWT library supports an algorithms whitelist.
5. Token Not Yet Valid — nbf Claim in the Future
The nbf (not before) claim sets the earliest time the token can be used. If the server's clock is behind the issuer's clock, the token will be rejected even though it was just created.
NotBeforeError: jwt not activeFix: Sync clocks across services using NTP. Most libraries also accept a clockTolerance option to allow a few seconds of skew.
6. Invalid Audience — aud Mismatch
The aud (audience) claim identifies who the token is intended for. If your server expects https://api.example.com but the token says https://staging-api.example.com, verification fails.
JsonWebTokenError: jwt audience invalid. expected: https://api.example.comFix: Ensure the aud claim in the token matches exactly what your verifier expects. This often breaks when tokens from a staging environment leak into production, or when an OAuth provider is configured with the wrong audience URL.
7. Invalid Issuer — iss Mismatch
Similar to audience errors, the iss (issuer) claim identifies who created the token. If you expect tokens from https://auth.example.com but receive one from https://auth.other-service.com, it will be rejected.
JsonWebTokenError: jwt issuer invalid. expected: https://auth.example.comFix: Check the iss value in your auth provider's configuration. For multi-tenant setups, make sure you're validating against the correct tenant's issuer URL. Decode the token to see the actual iss value and compare it against your config.
8. Clock Skew Issues
Distributed systems often have slight clock differences between machines. A token issued by Server A with exp set to 30 seconds from now might already appear expired to Server B if B's clock is 45 seconds ahead. This also causes intermittent nbf and iat validation failures.
// Token works on one server but fails on another
// Or fails intermittently — sometimes valid, sometimes expired
TokenExpiredError: jwt expired
// Token exp: 1710787200, Server time: 1710787203 (3 seconds past)Fix: Use NTP to synchronize clocks across all services. Add a clock tolerance (30-60 seconds) to your verification config. In Kubernetes, ensure all nodes have NTP configured at the host level.
How to Debug JWT Errors: Step-by-Step
When you hit a JWT error, resist the urge to change code first. Decode the token first to understand what you're working with.
- 1Decode the token without verification. Paste the raw JWT into our JWT Decoder to see the header, payload, and signature. This works even if the token is expired or the signature is invalid — you're just reading the Base64-decoded contents.
- 2Check the
expandiattimestamps. Convert them to human-readable dates. Isexpin the past? Wasiatreasonable? A token withiatfar in the future suggests a clock sync problem on the issuing server. - 3Verify the algorithm matches the key type. If the header says
RS256, you need an RSA public key for verification — not an HMAC secret string. If it saysHS256, you need the shared secret. Mixing these up is one of the most common causes of "invalid signature" errors. - 4Compare
issandaudclaims against your config. Copy the exact strings from the decoded token and diff them against the values in your server configuration. Watch for trailing slashes, protocol differences (httpvshttps), and port numbers. - 5Check for clock skew between services. Run
date -uon both the token issuer and the verifier. Even a 30-second difference can cause intermittent failures with short-lived tokens. In containers, the host clock may drift if NTP is not configured.
Fixing JWT Errors in Code
Here are practical patterns for handling JWT errors gracefully in the three most common server-side languages.
Node.js (jsonwebtoken)
import jwt from "jsonwebtoken";
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ["HS256"], // Whitelist allowed algorithms
issuer: "https://auth.example.com",
audience: "https://api.example.com",
clockTolerance: 30, // Allow 30 seconds of clock skew
});
// Token is valid — proceed with decoded.sub, decoded.role, etc.
} catch (err) {
if (err.name === "TokenExpiredError") {
// Token expired — prompt client to refresh
return res.status(401).json({ error: "token_expired" });
}
if (err.name === "JsonWebTokenError") {
// Invalid signature, malformed token, bad algorithm
return res.status(401).json({ error: "invalid_token" });
}
if (err.name === "NotBeforeError") {
// Token not yet active
return res.status(401).json({ error: "token_not_active" });
}
}Python (PyJWT)
import jwt
from jwt.exceptions import (
ExpiredSignatureError,
InvalidSignatureError,
InvalidAudienceError,
InvalidIssuerError,
DecodeError,
ImmatureSignatureError,
)
try:
decoded = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
audience="https://api.example.com",
issuer="https://auth.example.com",
leeway=30, # Allow 30 seconds of clock skew
)
except ExpiredSignatureError:
# Token has expired — trigger refresh flow
return {"error": "token_expired"}, 401
except InvalidSignatureError:
# Signature doesn't match — wrong key or tampered token
return {"error": "invalid_signature"}, 401
except InvalidAudienceError:
# aud claim doesn't match expected audience
return {"error": "invalid_audience"}, 401
except InvalidIssuerError:
# iss claim doesn't match expected issuer
return {"error": "invalid_issuer"}, 401
except ImmatureSignatureError:
# nbf claim is in the future
return {"error": "token_not_active"}, 401
except DecodeError:
# Malformed token — not valid Base64 or not 3 parts
return {"error": "malformed_token"}, 401Go (golang-jwt)
import (
"errors"
"github.com/golang-jwt/jwt/v5"
)
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
// Enforce algorithm — reject anything that isn't HS256
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secretKey), nil
},
jwt.WithIssuer("https://auth.example.com"),
jwt.WithAudience("https://api.example.com"),
jwt.WithLeeway(30 * time.Second),
)
if err != nil {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
// Handle expired token
case errors.Is(err, jwt.ErrSignatureInvalid):
// Handle invalid signature
case errors.Is(err, jwt.ErrTokenNotValidYet):
// Handle nbf in the future
default:
// Handle malformed or other errors
}
}JWT Security Best Practices
Fixing errors is one thing — preventing security vulnerabilities is another. Follow these rules to keep your JWT implementation secure:
- Never use
alg: "none"in production. Always pass an explicitalgorithmswhitelist to your verify function so attackers cannot force an unsigned token through. - Always validate
exp,iss, andaud. These three claims are your primary defense against token misuse. Skipping any one of them opens a window for token replay or cross-service attacks. - Use RS256 over HS256 for distributed systems. With HMAC (HS256), every service that verifies tokens must have the shared secret — a single compromised service exposes the key. RSA (RS256) lets you keep the private key on the auth server and distribute only the public key.
- Rotate signing keys periodically. Use a JWKS (JSON Web Key Set) endpoint so consumers can fetch updated keys automatically. Include a
kid(key ID) in the header so verifiers know which key to use during rotation. - Keep tokens short-lived. Access tokens should expire in 15 minutes or less. Use refresh tokens (7-day lifetime, stored securely) to issue new access tokens without forcing the user to log in again.
- Never put sensitive data in the payload. JWTs are encoded, not encrypted. Anyone can decode the payload with a Base64 decoder. Store only identifiers and permissions — never passwords, API keys, or personal data.
Running auth services that issue JWTs?
Cloudways gives you managed cloud hosting with built-in SSL, automated backups, and server-level firewalls — so your auth endpoints stay protected while you focus on your application logic. Deploy on DigitalOcean, AWS, or GCP without managing servers yourself.
Debug Your Tokens Now
The fastest way to diagnose a JWT error is to look at the token itself. Paste any JWT into our JWT Decoder to instantly see the header, payload, expiration status, and signature — all decoded locally in your browser. Need to create test tokens? Use the JWT Builder to generate signed tokens with custom claims, algorithms, and expiration. You can also use the Base64 Encoder to manually decode individual JWT segments, or the Hash Generator to explore the SHA-256 algorithm that underpins JWT signatures.