By AndyPublished
Common JWT Errors and How to Fix Them
Malformed JWT / Invalid Format
Symptoms
Library throws: JsonWebTokenError: jwt malformed, Invalid token format, or Not enough or too many segments. The decoder on this tool shows "Malformed JWT" or a segment count other than 3/3.
Root causes
- ·Token was truncated when copied from a log, URL, or storage
- ·Extra whitespace, newlines, or line breaks were included when copying
- ·The "Bearer " prefix from an Authorization header was left in
- ·The token was double-encoded (URL-encoded or JSON-stringified)
- ·A non-JWT string (opaque session token, API key) was passed to a JWT parser
Fixes
- ·Verify the token has exactly three dot-separated segments
- ·Strip the
Bearerprefix before parsing - ·Trim whitespace from both ends of the token string
- ·If the token came from a URL, URL-decode it first
- ·If the token was stored as a JSON string, parse the JSON to get the raw token
Invalid Signature
Symptoms
Library throws: JsonWebTokenError: invalid signature, Signature verification failed, or JWT signature does not match.
Root causes
- ·Wrong secret or key was used for verification
- ·The key is correct but in the wrong format (e.g., raw string vs. base64-encoded secret)
- ·The token payload was modified after signing
- ·The algorithm used for verification doesn't match the
algheader - ·Key rotation happened and you're using an old key, check the
kidheader - ·The token was signed with a private key but you have a different private key (not the public key)
Fixes
- ·Confirm the exact secret or key used by the issuer matches what you're using for verification
- ·Check the
algheader claim and match it to your library configuration - ·If using RS256/ES256, ensure you have the public key (not the private key) for verification
- ·Use the
kidheader to look up the correct key from the issuer's JWKS endpoint - ·If the issuer recently rotated keys, fetch a fresh copy of the JWKS
Expired Token (jwt expired)
Symptoms
Library throws: TokenExpiredError: jwt expired. The Claims tab shows an "Expired" badge on the exp claim.
Root causes
- ·The token's
exptimestamp is in the past - ·Client clock is significantly ahead of the server clock
- ·Token was cached too aggressively and is being reused after it expired
- ·Refresh token flow is not working, so expired access tokens aren't being replaced
Fixes
- ·In development: issue a new token or increase the expiry for testing (but fix the root cause)
- ·Check server and client clock synchronization (NTP)
- ·Verify your refresh token flow is being triggered before access tokens expire
- ·In production: never just extend the expiry to work around the issue, investigate why tokens aren't being refreshed
Not Yet Valid (jwt not active)
Symptoms
Library throws: NotBeforeError: jwt not active. The Claims tab shows "Not Yet Valid" on the nbf claim.
Root causes and fixes
- ·The
nbfclaim is set to a future time, this is intentional if the token should only activate later - ·Clock skew between issuer and verifier, add a clock skew tolerance of 30–60 seconds in your library configuration
- ·If
nbfis unintentionally in the future, check the issuer's clock and token generation logic
Wrong Audience
Symptoms
Library throws: JsonWebTokenError: jwt audience invalid or Audience validation failed.
Root causes and fixes
- ·Your service's audience identifier doesn't match the
audclaim in the token - ·Token was issued for a different service and is being replayed against your API
- ·Your audience configuration uses a different format (URL vs. client ID) than the issuer uses
- ·Fix: confirm the exact audience value your issuer includes and configure your library to match it exactly
Wrong Issuer
Symptoms
Library throws: JsonWebTokenError: jwt issuer invalid or Issuer validation failed.
Root causes and fixes
- ·The token's
issclaim doesn't match your expected issuer string - ·Common mismatch: trailing slash in one but not the other (
https://auth.example.comvshttps://auth.example.com/) - ·Token was issued by a different environment (staging vs production)
- ·Fix: copy the exact issuer URL from a known-good token's
issclaim and use that as your expected value
Clock Skew Errors
Clock skew occurs when the clocks on the issuing server and the verifying server are not synchronized. Even a difference of 2–3 minutes can cause tokens to appear expired immediately after issuance, or not-yet-valid when they should be active.
Why clocks drift
NTP (Network Time Protocol) keeps server clocks accurate, but drift is inevitable. In cloud environments a 1–5 second drift is normal. In Docker containers or VMs that are paused and resumed, the internal clock can drift significantly, sometimes by minutes, until the next NTP sync. A container that is not configured to use the host clock or an NTP server will drift continuously.
Diagnosing clock skew
Paste the token into this tool. In the Claims tab, look at the exp and iat timestamps. If the token shows as expired but was just issued, the difference between the current time and exp tells you the approximate clock skew. If the token expires 1 hour after issuance but shows as expired 2 minutes after it was issued, the verifier's clock is running ~58 minutes fast.
Fixes
- ·Configure clock skew tolerance in your library: Most JWT libraries accept a
clockToleranceorleewayoption. Set it to 30–60 seconds. Do not set it higher, a large tolerance defeats the purpose of short-lived tokens. - ·On Linux servers: Verify NTP is running with
timedatectl status. IfNTP service: inactive, enable it:timedatectl set-ntp true. - ·In Docker containers: Containers share the host kernel clock by default. If the host clock is correct, container clocks should match. Verify the host NTP sync first. If the container is running in a VM, also verify the hypervisor clock sync.
- ·In Kubernetes: Pod clocks inherit from the node. Check node time sync with
kubectl get nodes -o wideand verify the node's NTP configuration in your cloud provider's OS configuration.
# Check NTP status (Linux) timedatectl status # Force NTP sync immediately (systemd-timesyncd) systemctl restart systemd-timesyncd # Check clock offset (chrony) chronyc tracking | grep "System time" # In a Docker container, check if host clock is correct date && docker run --rm alpine date
Unsupported Algorithm
Symptoms
Library throws: Invalid algorithm, Algorithm not supported, or the decoder shows an unrecognized alg value.
- ·The token uses an algorithm your library doesn't support, check the library's documentation for supported algorithms
- ·Your library is configured to only accept specific algorithms and the token's
algis not in that list - ·The token has
"alg": "none": this should always be rejected - ·Fix: update the library or configure the allowed algorithm list to include the required algorithm
Debugging JWT Issues with Browser DevTools
Browser DevTools is the fastest way to capture a live token from a web application and diagnose what's wrong with it. Here is the standard workflow:
Step 1: Capture the token from a network request
- ·Open DevTools (F12 or Cmd+Option+I on Mac)
- ·Go to the Network tab
- ·Perform the action that triggers the failing API call (e.g., load the page, click a button)
- ·Find the failing request in the network list, look for a 401 or 403 status
- ·Click the request and go to the Headers tab
- ·Find the
Authorizationheader, the value looks likeBearer eyJhbGci... - ·Copy everything after "Bearer " (the three-segment JWT)
Step 2: Inspect the token
Paste the copied token into this tool. Check:
- ·Claims tab: Is
expin the past? Doesaudmatch your API? Doesissmatch the expected issuer? - ·Decoded tab: Are the header fields (
alg,kid) what you expect? - ·Verify tab: If you have the signing key, verify the signature to rule out tampering
Step 3: Check storage
If the token in the Authorization header looks correct but the request still fails, check where the token is coming from. In the DevTools Application tab:
- ·Cookies: If the token is in a cookie, check the cookie's domain, path, and expiry
- ·Local Storage / Session Storage: If the token is stored here, check whether it has been refreshed recently
// Find all JWTs in localStorage (browser console)
Object.keys(localStorage)
.filter(k => localStorage.getItem(k)?.startsWith('eyJ'))
.forEach(k => console.log(k, localStorage.getItem(k)))Environment-Specific Issues
A distinct class of JWT errors stems not from the token itself but from mismatches between the configuration of different deployment environments. These errors are often misdiagnosed as JWT library bugs.
Staging vs production token mix-ups
A token issued by a staging auth server (e.g., iss: https://staging-auth.example.com/) will fail validation against a production API configured to accept only iss: https://auth.example.com/. This happens most often when a developer copies a token from a staging environment and tests it against a production endpoint, or when environment variables are misconfigured in a deployment pipeline.
Diagnose: Check the iss claim in the token and compare it to the EXPECTED_ISSUER configuration in your API service. They must match exactly (including trailing slashes and protocol).
Mismatched iss due to environment config
When services are containerised, the auth service URL seen by the issuer may differ from the URL expected by the verifier. For example, an auth service running inside a Docker Compose network might issue tokens with iss: http://auth-service:8080/ (an internal hostname), while an API service configured for production expects iss: https://auth.example.com/.
Fix: Configure the auth service to always issue tokens with the public-facing issuer URL, regardless of the internal hostname used for the service. The iss claim should reflect the external, canonical URL of the auth server.
Docker container clock drift causing exp failures
Docker containers can experience clock drift after the host is suspended, migrated, or restarted. If a container is running in a development environment (a laptop that is suspended and resumed), the container's clock can drift by minutes before the next NTP sync. This causes tokens with short expiry times to fail immediately.
- ·Verify host clock accuracy:
dateon the host vs. inside the container - ·Restart the container after a host suspend/resume to force a clock re-sync
- ·In CI environments, ensure the runner's NTP is active before running JWT integration tests