Debugging JWT Authentication Errors: A Systematic Guide

May 28, 20267 min read

JWT Errors Are Cryptic by Default

When JWT authentication fails, you typically get a generic 401 response with little explanation. The token "doesn't work," but the server rarely tells you why. This guide maps common failure patterns to their root causes so you can debug methodically instead of guessing.

Decode the Token First

Before anything else, decode the token and inspect its claims. You need to see what the token contains — and what it doesn't.

// Quick browser decode (verification only — no signature check)
function decodeJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid JWT structure');
  const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
  return payload;
}

Check these fields immediately:

  • exp — Is the token expired?
  • iss — Does it match what the server expects?
  • aud — Does it include this API's identifier?
  • iat — When was it issued? Is the timestamp plausible?

Error 1: Token Expired

Symptoms

  • Server returns 401 with message like "jwt expired" or "Token is expired"
  • The token worked recently but stopped

Diagnosis

const payload = decodeJWT(token);
const now = Math.floor(Date.now() / 1000);
const expired = now >= payload.exp;
const expiresAt = new Date(payload.exp * 1000).toISOString();

console.log(`Token expired: ${expired}`);
console.log(`Expires at: ${expiresAt}`);
console.log(`Current time: ${new Date().toISOString()}`);

Common Causes

CauseFix
Access token expired normallyImplement token refresh flow
Clock skew between serversAllow 30-60 second leeway in exp check
Token issued with wrong expCheck auth server's token lifetime configuration
Client clock misconfiguredSync system time via NTP

Clock Skew Fix

const jwt = require('jsonwebtoken');

// Allow 30 seconds of clock skew
jwt.verify(token, secret, { clockTolerance: 30 });

Error 2: Invalid Signature

Symptoms

  • Server returns 401 with "invalid signature" or "jwt malformed"
  • Token was not tampered with, but verification still fails

Diagnosis

This is the most confusing error because the token looks valid when decoded. The payload is readable, but the signature doesn't match.

Common Causes and Fixes

Wrong secret key (HS256):

// Make sure the secret exactly matches what the issuer uses
// Include any whitespace or encoding issues
const secret = process.env.JWT_SECRET; // Verify this matches
jwt.verify(token, secret);

Wrong public key (RS256):

// Fetch the correct public key from the issuer's JWKS endpoint
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});

const key = await client.getSigningKey(kid);
const publicKey = key.getPublicKey();
jwt.verify(token, publicKey);

Algorithm mismatch: The token header specifies alg: "RS256", but the verifier defaults to HS256. Always specify the expected algorithm:

// WRONG — lets the header dictate the algorithm (alg: none attack)
jwt.verify(token, publicKey);

// CORRECT — explicitly specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Error 3: Audience Mismatch

Symptoms

  • "jwt audience invalid" or "Audience not allowed"
  • Token works for one API but not another

Diagnosis

const payload = decodeJWT(token);
console.log('Token audience:', payload.aud);
console.log('Expected audience:', expectedAud);

Common Causes

  • The auth server was configured with the wrong audience identifier
  • Multiple APIs share tokens but have different audience requirements
  • The aud claim is an array, but the verifier expects a string (or vice versa)

Fix

// Allow multiple audiences
jwt.verify(token, secret, {
  audience: ['https://api.example.com', 'https://admin.example.com']
});

Error 4: Issuer Mismatch

Symptoms

  • "jwt issuer invalid"
  • Token was issued by a different identity provider than expected

Diagnosis

const payload = decodeJWT(token);
console.log('Token issuer:', payload.iss);
console.log('Expected issuer:', expectedIss);
// Common issue: trailing slash mismatch
// "https://auth.example.com/" !== "https://auth.example.com"

Error 5: Key Rotation Issues

Symptoms

  • Tokens suddenly fail after a key rotation event
  • Some tokens work and others don't

Diagnosis

When the issuer rotates signing keys, the kid (key ID) in the JWT header identifies which key was used. If your application caches the public key, the cached key may be stale.

Fix

// Use jwks-rsa with caching and rate limiting
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 86400000, // 24 hours
  rateLimit: true,
  jwksRequestsPerMinute: 10
});

Always extract the kid from the header and look up the corresponding key:

const decoded = jwt.decode(token, { complete: true });
const kid = decoded.header.kid;
const signingKey = await client.getSigningKey(kid);

Debugging Checklist

When a JWT fails, check these in order:

  1. Structure: Does the token have exactly three dot-separated parts?
  2. Expiration: Is exp in the past?
  3. Not Before: Is nbf in the future?
  4. Issuer: Does iss match the expected value (exact string, including trailing slashes)?
  5. Audience: Does aud include this API's identifier?
  6. Algorithm: Does the alg header match what the verifier expects?
  7. Signature: Is the correct key being used? Check kid against JWKS.
  8. Clock Skew: Are the client and server clocks synchronized?

Key Takeaways

  • Always decode and inspect the JWT payload before debugging authentication code
  • Clock skew between servers is the most common cause of unexpected exp failures
  • Specify allowed algorithms explicitly during verification to prevent algorithm confusion attacks
  • Audience and issuer mismatches often involve subtle string differences (trailing slashes, http vs https)
  • Key rotation requires JWKS-based key lookup — never hardcode public keys
  • Follow the debugging checklist in order — most JWT errors have straightforward root causes

Try It Yourself

Decode and inspect any JWT instantly with our free JWT Decoder. Paste a token to see its header, payload, and all claims — everything is processed locally in your browser with no server communication.

Try the JWT Decoder →