JWT debugging is surprisingly painful. The error messages from JWT libraries are terse, the base64url encoding obscures the payload, and issues like algorithm confusion attacks or misconfigured JWKS URIs require deep framework knowledge. AI tools dramatically speed up diagnosis. This guide shows practical prompting patterns for common JWT failure modes.
Common JWT Failure Categories
- Signature verification failures — wrong secret, wrong algorithm, RS256/HS256 confusion
- Claim validation errors — expired
exp, wrongiss, wrongaud,nbfin the future - Malformed tokens — incorrect padding, encoding issues, truncated tokens
- Algorithm confusion attacks — RS256 public key used as HS256 secret
- JWKS endpoint issues — key rotation failures, kid mismatch, network errors
Debugging Workflow
Always start by decoding the token. Never paste real production tokens — sanitize first.
# decode_jwt.py — decode without verification for debugging
import base64
import json
import sys
def decode_jwt_parts(token: str) -> dict:
"""Decode JWT header and payload without signature verification."""
parts = token.split('.')
if len(parts) != 3:
return {"error": f"Expected 3 parts, got {len(parts)}"}
def decode_part(part: str) -> dict:
# Fix base64url padding
padding = 4 - len(part) % 4
if padding != 4:
part += '=' * padding
decoded = base64.urlsafe_b64decode(part)
return json.loads(decoded)
return {
"header": decode_part(parts[0]),
"payload": decode_part(parts[1]),
"signature_length": len(parts[2]),
}
if __name__ == "__main__":
token = sys.argv[1] if len(sys.argv) > 1 else input("Paste token: ").strip()
result = decode_jwt_parts(token)
print(json.dumps(result, indent=2, default=str))
Run this first, then paste the output (never the raw token) to an AI tool for analysis.
Prompting Claude for JWT Diagnosis
Example scenario: Token rejected with “invalid signature”
Decoded header:
{ "alg": "HS256", "typ": "JWT" }
Decoded payload:
{
"sub": "user_123",
"iss": "https://auth.example.com",
"aud": "api.example.com",
"exp": 1742000000,
"iat": 1741996400
}
Prompt:
JWT is being rejected with "invalid signature" by my Node.js Express app using
the `jsonwebtoken` library. The token was issued by Auth0.
Header: {"alg": "HS256", "typ": "JWT"}
Payload: [decoded payload above]
My verification code:
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] })
The JWT_SECRET env var is set to our Auth0 Application Client Secret.
What are the likely causes?
Claude’s diagnosis:
Claude identifies this immediately as an algorithm mismatch issue: Auth0 issues RS256 tokens (asymmetric) by default for APIs, not HS256 (symmetric). The HS256 in the header likely means the token was generated with a different configuration or the client secret is being passed where a public key is required.
Claude’s suggested fix:
// Option 1: Fetch Auth0's JWKS and use RS256
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: `https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json`,
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 600000, // 10 minutes
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
jwt.verify(token, getKey, { algorithms: ['RS256'], audience: 'api.example.com' }, (err, decoded) => {
if (err) console.error('Verification failed:', err.message);
else console.log('Decoded:', decoded);
});
// Option 2: If you actually want HS256, change Auth0 API settings
// Auth0 Dashboard → APIs → Your API → Settings → Token Signing Algorithm → HS256
// Then use the API identifier's client secret, NOT the application secret
Algorithm Confusion Attack Debugging
This is a real security issue. Claude handles it precisely:
Prompt: “I’m seeing tokens succeed verification that shouldn’t. My server uses RS256. Could an attacker be exploiting algorithm confusion?”
Claude’s explanation and mitigation:
// VULNERABLE: Not explicitly specifying algorithms
jwt.verify(token, publicKey); // Attacker can send HS256 token signed with publicKey as HMAC secret
// SECURE: Always specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Also validate the alg header explicitly before verification
function verifyTokenSecurely(token: string, publicKey: string): JwtPayload {
const header = JSON.parse(
Buffer.from(token.split('.')[0], 'base64url').toString()
);
if (header.alg !== 'RS256') {
throw new Error(`Rejected: unexpected algorithm ${header.alg}`);
}
return jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
}) as JwtPayload;
}
JWKS Key Rotation Debugging
A common production issue: JWKS key rotation causes sudden 401s.
# jwks_debugger.py — check if kid matches any key in JWKS endpoint
import requests
import base64
import json
import sys
def debug_jwks_mismatch(token: str, jwks_uri: str) -> None:
# Decode header
header_b64 = token.split('.')[0]
padding = 4 - len(header_b64) % 4
if padding != 4:
header_b64 += '=' * padding
header = json.loads(base64.urlsafe_b64decode(header_b64))
token_kid = header.get('kid')
token_alg = header.get('alg')
print(f"Token kid: {token_kid}, alg: {token_alg}")
# Fetch JWKS
resp = requests.get(jwks_uri, timeout=10)
resp.raise_for_status()
jwks = resp.json()
available_kids = [key.get('kid') for key in jwks.get('keys', [])]
print(f"Available kids in JWKS: {available_kids}")
if token_kid in available_kids:
print("OK: kid found in JWKS")
else:
print(f"MISMATCH: kid '{token_kid}' not found in JWKS endpoint")
print("This means the token was signed with a key that has been rotated out.")
print("Action: Re-issue the token or check if JWKS caching is stale.")
if __name__ == "__main__":
debug_jwks_mismatch(sys.argv[1], sys.argv[2])
Paste this script + the output to Claude:
Running this script gave:
Token kid: abc123
Available kids in JWKS: [xyz789, def456]
MISMATCH: kid 'abc123' not found
This is a production API. Users are getting 401s after a deploy. What happened and how do I fix it?
Claude will identify this as a key rotation without cache invalidation and provide the mitigation: extend JWKS cache TTL but add an explicit re-fetch on kid mismatch, then force re-auth for affected sessions.
Debugging Clock Skew and Expiry Issues
Clock skew between services causes intermittent jwt expired and jwt not active errors that are hard to reproduce. Claude generates a diagnostic script:
# check_jwt_timing.py — analyze exp/iat/nbf claims against current time
import base64
import json
import sys
import time
from datetime import datetime, timezone
def analyze_jwt_timing(token: str) -> None:
parts = token.split('.')
padding = 4 - len(parts[1]) % 4
if padding != 4:
parts[1] += '=' * padding
payload = json.loads(base64.urlsafe_b64decode(parts[1]))
now = time.time()
print(f"Current server time: {datetime.fromtimestamp(now, tz=timezone.utc).isoformat()}")
print(f"Current unix ts: {now:.0f}")
print()
for claim in ['iat', 'exp', 'nbf']:
if claim in payload:
ts = payload[claim]
dt = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
delta = ts - now
status = ""
if claim == 'exp':
status = "EXPIRED" if delta < 0 else f"expires in {delta:.0f}s"
elif claim == 'nbf':
status = "NOT YET VALID" if delta > 0 else "valid"
elif claim == 'iat':
status = f"issued {abs(delta):.0f}s {'ago' if delta < 0 else 'in the future (clock skew!)'}"
print(f"{claim}: {ts} ({dt}) — {status}")
if __name__ == "__main__":
token = sys.argv[1] if len(sys.argv) > 1 else input("Paste token: ").strip()
analyze_jwt_timing(token)
When iat is in the future by more than a second or two, that indicates the issuing server’s clock is ahead of the validating server. The fix is NTP sync, but the short-term workaround is adding a clockTolerance option:
// jsonwebtoken: allow up to 30 seconds of clock skew
jwt.verify(token, secret, {
algorithms: ['RS256'],
clockTolerance: 30, // seconds
});
Claim Validation Errors Reference
Claude can generate a debugging checklist when you describe validation error patterns:
| Error | Common Cause | AI-Generated Fix |
|---|---|---|
jwt expired |
exp in past |
Check server clock sync (NTP); add clock skew tolerance |
jwt not active |
nbf in future |
Clock skew between issuer and verifier |
invalid audience |
aud mismatch |
Pass correct audience option to verify() |
invalid issuer |
iss mismatch |
Check env var for issuer URL (trailing slash matters) |
invalid signature |
Wrong secret/key | Verify you’re using correct key type for alg |
jwt malformed |
Bad base64 encoding | Check for URL encoding of . or + in token |
Building a JWT Debugging Toolkit
Create a reusable CLI tool that handles common JWT problems and surfaces them to Claude:
#!/usr/bin/env python3
# jwt_debugger.py — comprehensive JWT diagnostics
import base64
import json
import sys
import requests
from typing import Tuple, Dict, Any
import anthropic
class JWTDebugger:
def __init__(self, token: str):
self.token = token.strip()
self.parts = token.split('.')
def is_valid_format(self) -> bool:
"""Check if token has 3 parts."""
return len(self.parts) == 3
def decode_safely(self) -> Dict[str, Any]:
"""Decode all parts without verification."""
if not self.is_valid_format():
return {"error": f"Token has {len(self.parts)} parts, expected 3"}
result = {}
for name, part in [("header", 0), ("payload", 1)]:
try:
# Fix base64url padding
padding = 4 - len(part) % 4
if padding != 4:
part += '=' * padding
decoded = base64.urlsafe_b64decode(part)
result[name] = json.loads(decoded)
except Exception as e:
result[name] = {"decode_error": str(e)}
result["signature_length"] = len(self.parts[2])
result["signature_hex"] = self.parts[2][:32] + "..."
return result
def check_expiration(self) -> Dict[str, Any]:
"""Check if token is expired."""
try:
payload = json.loads(
base64.urlsafe_b64decode(self.parts[1] + '==')
)
if 'exp' not in payload:
return {"has_exp": False}
from datetime import datetime
exp_time = datetime.fromtimestamp(payload['exp'])
now = datetime.now()
return {
"expires_at": exp_time.isoformat(),
"is_expired": now > exp_time,
"seconds_remaining": int((exp_time - now).total_seconds())
}
except Exception as e:
return {"error": str(e)}
def check_jwks(self, jwks_uri: str) -> Dict[str, Any]:
"""Verify token kid against JWKS endpoint."""
try:
header = json.loads(
base64.urlsafe_b64decode(self.parts[0] + '==')
)
token_kid = header.get('kid')
if not token_kid:
return {"error": "Token has no kid in header"}
resp = requests.get(jwks_uri, timeout=5)
resp.raise_for_status()
jwks = resp.json()
available_kids = [k.get('kid') for k in jwks.get('keys', [])]
return {
"token_kid": token_kid,
"available_kids": available_kids,
"kid_found": token_kid in available_kids
}
except Exception as e:
return {"error": str(e)}
def create_analysis_prompt(self, decoded: Dict, expiration: Dict,
jwks_check: Dict = None) -> str:
"""Format all diagnostics into a Claude prompt."""
analysis = f"""JWT Diagnostics Report
======================
Decoded Token:
{json.dumps(decoded, indent=2)}
Expiration Status:
{json.dumps(expiration, indent=2)}
"""
if jwks_check and 'error' not in jwks_check:
analysis += f"\nJWKS Key Status:\n{json.dumps(jwks_check, indent=2)}"
return analysis + "\n\nWhat are the issues with this token and how do I fix them?"
def diagnose(self, jwks_uri: str = None) -> str:
"""Run full diagnostics and get Claude's analysis."""
if not self.is_valid_format():
return f"Token format error: {len(self.parts)} parts instead of 3"
decoded = self.decode_safely()
expiration = self.check_expiration()
jwks_check = self.check_jwks(jwks_uri) if jwks_uri else None
prompt = self.create_analysis_prompt(decoded, expiration, jwks_check)
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: jwt_debugger.py <token> [--jwks-uri <uri>]")
sys.exit(1)
token = sys.argv[1]
jwks_uri = None
if "--jwks-uri" in sys.argv:
idx = sys.argv.index("--jwks-uri")
jwks_uri = sys.argv[idx + 1]
debugger = JWTDebugger(token)
analysis = debugger.diagnose(jwks_uri)
print(analysis)
Usage in production debugging:
# Diagnose a token from your app logs
./jwt_debugger.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Diagnose with JWKS validation
./jwt_debugger.py "eyJ..." --jwks-uri "https://auth.example.com/.well-known/jwks.json"
# Output example from Claude:
# Token is expired by 3 hours (exp: 2026-03-22T14:30:00)
# The token was issued for audience 'api.old.example.com' but you're verifying against 'api.example.com'
# Action: Re-authenticate user or update audience configuration
Common JWT Issues Reference Table
| Symptom | Root Cause | Test Command | Fix |
|---|---|---|---|
| 401 “invalid signature” | Wrong secret or key | Check if alg is RS256 or HS256 |
Verify secret rotation, check JWKS endpoint |
| “token expired” | exp claim in past | jwt_debugger.py --check-exp |
Re-issue token, check server time sync |
| “invalid audience” | aud mismatch | Decode payload, check aud field |
Ensure verify() gets correct audience |
| “invalid issuer” | iss mismatch | Decode payload, check iss field |
Verify issuer URL matches exactly (trailing slash matters) |
| “jwt malformed” | Base64 encoding issue | Run jwt_debugger.py first | Re-encode token, check for URL encoding artifacts |
| “kid not found” | Key rotation issue | jwt_debugger.py –jwks-uri |
JWKS cache too old, refresh or re-issue |
Testing JWT Integration with AI
Prompt Claude to generate test cases:
Generate Jest/Mocha test cases for verifying JWTs. Include:
1. Valid token verification
2. Expired token rejection
3. Wrong audience rejection
4. Algorithm confusion attack prevention
5. Key rotation (kid mismatch)
6. Clock skew tolerance
Use Auth0 as the issuer for examples.
Claude generates the full test suite with mocks, edge cases, and security assertions.
JWT Tools Comparison
| Tool | Debugging Speed | Accuracy | Learning Curve |
|---|---|---|---|
| Claude (with diagnostics) | Fast (30-60s) | 95%+ | Low (natural language) |
| jwt.io web tool | Instant | Manual inspection | Low (visual) |
| Manual base64 decode | Slow (2-5 min) | 70% (easy to misread) | High |
| Dedicated JWT debuggers | Moderate (1-2 min) | 80% (limited context) | Moderate |
Claude wins because it connects the dots between multiple issues (wrong audience + expired = authentication problem, not authorization).
Related Reading
- How to Use AI to Debug Segmentation Faults in C and C++ Programs
- AI Assistants for Writing Correct AWS IAM Policies with Least Privilege
- AI Tools for Automated SSL/TLS Configuration
- AI Debugging Assistants Compared 2026
Built by theluckystrike — More at zovo.one