← Debugging Guides

阅读中文版本

JWT — token expired error even with a fresh login

Diagnose JWT expiry errors by checking the exp claim, clock skew and token refresh timing.

intermediatedebuggingjwtauth

Symptoms

  • TokenExpiredError: jwt expired
  • The user is logged out almost immediately
  • It fails on the server but not locally
  • Refresh logic does not run before expiry

Possible causes

  • The token TTL is very short and clocks differ between issuer and verifier
  • Server time is ahead of the token issuer time
  • Refresh is attempted only after a request already failed
  • The exp claim is interpreted in the wrong unit

Diagnosis steps

Decode the claims without verifying

node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64'))" <token>

Interpretation

Reading iat and exp shows the intended lifetime and whether it is already past.

Compare clocks

date -u

Interpretation

A verifier clock ahead of the issuer makes a valid token appear expired, which is common in distributed systems.

Allow a small leeway

jwt.verify(token, secret, { clockTolerance: 5 })

Interpretation

A few seconds of tolerance absorbs minor skew without weakening security.

Root cause

The token was considered expired by the verifier because its exp claim was in the past, either genuinely due to a short TTL, or apparently because the verifier clock ran ahead of the issuer clock.

Fix

Set a TTL that matches the use case, synchronize clocks across services, allow a small clock tolerance, and refresh tokens proactively before they expire rather than after a failed request.

Verify the fix

Log in and use the token through its expected lifetime and confirm it is accepted, and confirm refresh happens before expiry under normal use.

Symptom walkthrough

A request fails with TokenExpiredError even though the user just logged in. Decoding the token shows its exp claim, and the question is whether the token genuinely expired or the verifier thinks so because its clock is ahead of the issuer. Very short TTLs combined with clock skew between services make valid tokens appear expired, which is confusing and easy to misattribute to a login bug.

Fix in practice

Choose a TTL that fits the session, keep clocks synchronized across services, and permit a small clock tolerance to absorb minor skew. Refresh tokens before they expire based on their remaining lifetime rather than reacting to a failed request, which avoids a window of errors.

Verification

Log in and exercise the token across its expected lifetime and confirm it is accepted, then confirm refresh occurs before expiry during normal traffic. Acceptance through the lifetime plus proactive refresh confirms the fix.

Diagnostic commands

Check the behavior with commands like these:

node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64'))" <token>
date -u
jwt.verify(token, secret, { clockTolerance: 5 })