Decoding and Debugging JWTs: A Developer's Checklist
JWT authentication issues are frustrating to debug because the symptoms are vague — mysterious 401s, intermittent failures, users unexpectedly logged out — and error messages from libraries aren't always specific. This checklist gives you a systematic approach.
Decode any JWT: See header, payload, claims, and expiry status — all in your browser.
Open JWT Decoder →Table of Contents
Step 1: Decode the Token and Read the Claims
Before anything else, decode the token and check its actual contents. At least half of JWT debugging issues are resolved here — the token simply contains wrong or missing claims because of a bug in token generation code.
- What algorithm does the header specify (
alg)? - Has the token expired (
expin the past)? - Is the issuer (
iss) what the server expects? - Is the audience (
aud) correct for this service? - Are custom claims (roles, permissions) present and correct?
Expiry Issues
Clock skew between servers
If auth and API servers have clocks differing by more than a few seconds, just-issued tokens may appear expired. Most JWT libraries have configurable clock skew tolerance (typically 60 seconds). Ensure all servers sync via NTP.
Detecting expiry manually
const payload = JSON.parse(atob(token.split('.')[1]));
const expiry = new Date(payload.exp * 1000);
console.log(`Expires: ${expiry.toLocaleString()}`);
console.log(`Expired: ${expiry < new Date()}`);
Signature Verification Failures
A signature failure means the token was tampered with, was signed with a different key, or the verifier is using the wrong key. For HS256, the same secret must sign and verify. For RS256, services use the public key — ensure it's current after key rotation. Many auth servers include a kid (key ID) in the header to identify which key to use from the JWKS endpoint.
Always Specify the Algorithm Explicitly
// BAD — accepts any algorithm (vulnerable to algorithm confusion)
jwt.verify(token, secret);
// GOOD — specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
Claim Validation Failures
- Audience mismatch: verify
audmatches your service — a token for your mobile app shouldn't be accepted by your admin API - Issuer mismatch: validate
iss— staging tokens should not be accepted in production - Missing custom claims: validate role/permission claims are present after signature verification
Full Debugging Checklist
- Decode the token — read all claims first
- Check
exp— is the token expired? Is your server clock correct? - Check the algorithm in the header against your verifier's allowlist
- Check the signing key — is it correct? Has it been rotated?
- Validate
issandaud - Check the Authorization header format:
Bearer <token>with a space - Check for token truncation from copy-paste or URL encoding
- Check the refresh flow — is the client refreshing before expiry?
- Enable verbose logging in your JWT library
Security Reference: RFC 8725
When a JWT is not just failing to decode but is causing a security concern — unexpected access, tokens that should be expired still working — refer to RFC 8725 (JSON Web Token Best Current Practices) for authoritative guidance. Key issues it addresses:
- Algorithm confusion attacks — An attacker modifies the
algheader tononeor switches from RS256 to HS256, using the public key as the HMAC secret. Fix: always explicitly specify and validate the expected algorithm server-side — never trust thealgclaim in the token header. - Missing audience validation — A token issued for service A is presented to service B. Fix: always validate the
aud(audience) claim and reject tokens not addressed to your service. - Missing issuer validation — A token from a different issuer passes signature verification. Fix: always validate
issagainst your expected issuer URL. - Overly long expiry — Tokens valid for days or weeks give attackers a long window if stolen. Fix: keep access tokens short-lived (15 minutes recommended); use refresh token rotation for persistence.
// Secure JWT verification (Node.js / jsonwebtoken)
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // explicit whitelist — never omit this
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
}, (err, payload) => { ... });
