DEV Community

EvvyTools
EvvyTools

Posted on

How to Debug JWT Authentication Failures Methodically

JWT authentication failures are frustrating because the error messages rarely say anything useful. A 401 Unauthorized tells you something failed. It does not tell you whether the token expired, the signature was invalid, the audience was wrong, or the token was never sent at all. Debugging them without a method means guessing, and guessing usually adds an hour to the investigation.

Here is a systematic approach that starts with the most common causes and eliminates them in order.

Step 1: Confirm the Token Is Being Sent

Before debugging the token itself, verify it is reaching the server. This sounds obvious but is frequently the actual problem. A token might fail to be stored after login, fail to be attached to the request, or be sent in the wrong header format.

Check the request in your browser's network tab or your API client. Look for the Authorization header. It should look like:

Authorization: Bearer eyJhbGci...
Enter fullscreen mode Exit fullscreen mode

If the header is missing, the problem is in your client code, not your token logic. If the header is present but uses a different format (some APIs use Token instead of Bearer, or the token is sent as a query parameter), that is also worth checking against the API's requirements.

If you are working in a CORS environment, preflight OPTIONS requests do not include credentials. Confirm the actual request, not the preflight, is carrying the header.

Step 2: Decode the Token and Check the Basics

Once you have the token string, decode it and look at three things first:

Is it expired? The exp claim is a Unix timestamp in seconds. Check whether it has passed. If the token expired two seconds ago, that is a token refresh problem. If it expired two days ago, something went wrong with how the token was stored or retrieved.

Does the aud claim match your API? If the token was issued for a different service or environment (staging vs production, for example), it will fail audience validation even though the signature is valid.

Does the iss claim match what your API expects? A mismatch here usually means tokens from a different authorization server are being used, which happens when environment configuration is wrong.

The EvvyTools JWT Decoder shows the decoded header and payload with real-time expiry countdown, which is faster than manual Base64 decoding and easier to read than raw JSON. For a full reference on JWT structure and what each claim means, the JWT token structure guide has the details.

Step 3: Verify the Algorithm Matches

The JWT header contains an alg field that specifies the signing algorithm. Your server is configured to expect a specific algorithm. If there is a mismatch -- the token claims RS256 but the server expects HS256, or vice versa -- verification will fail.

This sometimes happens when a library updates its default algorithm, or when tokens generated in one environment are used in another that was configured differently.

// node-jsonwebtoken: algorithm mismatch will throw JsonWebTokenError
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] })
Enter fullscreen mode Exit fullscreen mode

Pull the token header and look at alg. Then check your server's JWT configuration. They need to match.

Terminal screen showing JSON output from a decoded token
Photo by Pixabay on Pexels

Step 4: Check the Signature Key

Signature verification fails when the key used to verify does not match the key used to sign. This is especially common with asymmetric algorithms (RS256, ES256) when:

  • The wrong public key is being used for verification
  • The JWKS endpoint returned a different key than what was used to sign
  • The key was rotated and the old token was signed with the previous key

For HS256 (symmetric), the shared secret must be identical on both sides. A typo in an environment variable, a trailing newline in a secret file, or a base64 vs raw string difference will cause verification to fail silently.

To isolate this, try verifying the token with the key you believe is correct using a trusted library. If that fails, the key is wrong. If it succeeds, the problem is in your application's key configuration.

Step 5: Look at the Full Error Message From the Library

JWT libraries return specific error types. jwt.verify() in node-jsonwebtoken, for example, throws:

  • JsonWebTokenError: invalid signature -- key mismatch or token was tampered with
  • TokenExpiredError: jwt expired -- exp is in the past
  • JsonWebTokenError: jwt audience invalid -- aud does not match
  • JsonWebTokenError: jwt issuer invalid -- iss does not match
  • NotBeforeError: jwt not active -- nbf is in the future

These are all distinct conditions with different fixes. Logging just 401 Unauthorized without the underlying error type is a debugging dead end. Add error logging that includes the specific exception class or error message.

try {
  const decoded = jwt.verify(token, secret, options)
  // proceed
} catch (err) {
  console.error('JWT verification failed:', err.name, err.message)
  // now you know what went wrong
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Check for Clock Drift

Intermittent failures -- where the same token works sometimes but not others -- are often clock drift. JWT exp and nbf validation compares against the current server time. If one service's clock is ahead of another's by more than a few seconds, tokens that should be valid look expired.

Run date on each of your servers and compare. Configure NTP synchronization if clocks are drifting. As an immediate fix, configure a clock skew tolerance in your JWT library (usually a clockTolerance or clockSkew option expressed in seconds).

// Tolerate up to 60 seconds of clock skew
jwt.verify(token, secret, { clockTolerance: 60 })
Enter fullscreen mode Exit fullscreen mode

This is documented in RFC 7519 on the IETF datatracker as a recognized issue with time-based claims.

Step 7: Test Token Issuance Directly

If you have gone through all the above and still have failures, generate a fresh token using your authorization server directly and test it immediately. This isolates whether the problem is in token issuance or verification.

If a fresh token works but stored tokens do not, the problem is in storage or retrieval. If fresh tokens also fail, the problem is in your server's verification configuration. These two paths point to completely different fixes.

Server rack with status indicator lights in organized equipment bay
Photo by Brett Sayles on Pexels

Tools That Help

The EvvyTools JWT Decoder handles decoding and claim inspection in the browser. The token comparison feature is useful when you have a working token and a failing one and need to see what differs.

jwt.io has a debugger that also verifies signatures if you provide a key. Auth0 provides detailed documentation on their JWT implementation, which mirrors the behavior of most modern authorization servers. The OWASP Cheat Sheet Series covers common authentication failure patterns from a security perspective.

EvvyTools keeps these developer utilities in one place if you prefer to avoid context switching during debugging. Methodical debugging saves time -- the list above covers 95% of JWT failures, and most of them are visible in the token header or payload.

Building a Debugging Checklist for Your Team

Once you have debugged a JWT failure yourself, it is worth writing down the steps so teammates can resolve the same issues without escalating. A simple checklist -- is the header present, is the token expired, does the audience match -- saves time on both sides.

Consider adding a diagnostic endpoint in your development environment that accepts a token and returns a structured breakdown: whether the signature verified, which claims were found, whether the expiry is in the future, and which validation step failed if any. This is not a security endpoint and should be development-only, but it speeds up debugging significantly compared to reading raw JWT library error messages.

For production environments, structured logging that captures the specific JWT error type alongside the request path and user ID (not the token itself) creates an audit trail that makes authentication failure patterns visible over time. A spike in TokenExpiredError suggests a client-side refresh bug. A spike in JsonWebTokenError: invalid signature usually means a key rotation issue or environment mismatch.

Documentation from Auth0 on their JWT error handling patterns is a useful reference even if you are not using Auth0, because the error taxonomy mirrors what most standards-compliant authorization servers produce.

Top comments (0)