Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

clerk-js 6 token cache ages session tokens by server `iat` against the browser clock — slow client clocks resend expired JWTs on every request

Ouverte
#10,231 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
18/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active

Piste de recherche

The cache logic lives in packages/clerk-js/src/core/tokenCache.ts: start with setInternal, where createdAt is set from the JWT iat, and get(), where eviction compares against Date.now() and POLLER_INTERVAL_IN_MS. Reproduce by shifting Date.now back 120 s in a Playwright page and checking whether a backend stub rejects tokens past their exp. Done means evicting on receipt-time or offset-corrected time so the stub sees no expired tokens, with the refresh timer also corrected.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Preliminary checks
  • I have searched existing issues: no report mentions the token cache's clock dependence ("clock skew", "iat", "token cache").
Package + version

@clerk/clerk-js 6.39.0 (loaded from the CDN by @clerk/nextjs 7.9.12 / Core 3). Verified against the 6.39.0 and main sources and compared with @clerk/clerk-js 5.128.0.

Description

packages/clerk-js/src/core/tokenCache.ts (6.x) ages a cached session token from the server-issued iat against the browser clock:

  • after a token resolves, setInternal sets live.createdAt = issuedAt (the JWT iat) and computes remainingTtl = exp − Math.floor(Date.now()/1000) for the eviction timer;
  • get() evicts only when (expiresIn − (now − createdAt)) <= POLLER_INTERVAL_IN_MS/1000 (5 s), where now is Date.now() and createdAt/expiresIn come from the token.

Every term except now is server time, so the eviction point moves with the browser's clock error. In 5.x the same cache stamped a locally fetched token with nowSeconds on arrival (createdAt = entry.createdAt ?? nowSeconds, expiresIn = exp − iat) and evicted 15 s early, so a wrong browser clock did not matter: the token was dropped ~45 s after the browser received it regardless of skew.

Consequence on 6.x. A browser whose clock is S seconds slow believes a 60 s token has 60 + S seconds of life. getToken() keeps returning it, with no error, until 55 + S seconds after issue. A backend that validates exp with the usual 30 s leeway rejects it from 90 s on, so for S > 35 s every API call from that browser fails for (S − 35) seconds out of every (55 + S) — on a clock 5 minutes slow, about three quarters of all requests. The proactive refresh timer (refreshFireTime = remainingTtl − leeway − 2) is scheduled from the same skewed remainingTtl, so it fires too late as well. Fast clocks only cause early refreshes.

What we observed (production, after upgrading @clerk/nextjs 6.39 → 7.9, i.e. CDN clerk-js 5 → 6)
  • a few long-lived sessions sending an expired token on every background poll for 30–60 min, then recovering when the cache finally evicted;
  • user-initiated PATCH/DELETE calls answered 401 for 3–13 minutes straight on those sessions;
  • backend log flooded with JWT expired: Signature has expired (up to ~2,500 rows / 4 h); getToken() never threw on the client, so no frontend error was logged;
  • no Clerk-side incident: fresh tokens validated fine fleet-wide the whole time.
Reproduction

Playwright, Clerk dev instance, a signed-in page whose Date.now is shifted 120 s into the past by an init script, polling an API stub that decodes the bearer exp and answers 401 when the real clock is more than 30 s past it:

== clerk-js 6.39.0, no app-side workaround: skew=120s, 22 backend calls over 290s ==
window   200   401  tokens
 30-60      3     0  …kt0zl9VA
 60-90      1     0  …kt0zl9VA
 90-120     2     0  …kt0zl9VA
120-150     0     2  …kt0zl9VA
150-180     0     4  …kt0zl9VA
180-210     1     0  …Z7olUPNw
210-240     2     0  …Z7olUPNw
240-270     1     0  …Z7olUPNw
270-300     0     4  …Z7olUPNw
300-330     0     2  …Z7olUPNw
total: 22 calls, 12 rejected (55%), 2 distinct tokens

== same, with our skipCache-on-rejected workaround: skew=120s, 19 backend calls over 290s ==
window   200   401  tokens
 30-60      3     0  …MbqVOcNw
 60-90      1     0  …MbqVOcNw
 90-120     2     0  …MbqVOcNw
120-150     1     1  …MbqVOcNw …AFLN8Tyw
150-180     2     0  …AFLN8Tyw
180-210     1     0  …AFLN8Tyw
210-240     2     1  …AFLN8Tyw …jgxTSUHQ
240-270     1     0  …jgxTSUHQ
270-300     2     0  …jgxTSUHQ
300-330     1     1  …jgxTSUHQ …__qXlmuQ
total: 19 calls, 3 rejected (16%), 4 distinct tokens

(the API stub answers 401 once a token is >90 s old by the real clock; without the workaround every poll AND its retry carry the same stale token until clerk-js evicts it ~175 s after issue)
Expected behavior

Freshness measured on the browser clock from receipt (as 5.x did), or createdAt corrected by the observed server/client offset (iat vs Date.now() at receipt, or the Date header of the /tokens response).

Workaround we shipped

Record the bearer a backend 401 refused and have our getToken wrapper call getToken({ skipCache: true }) when the cache hands that same string back (one shared in-flight mint). Reactive, not a fix.

Langage dominant
TypeScript
Étoiles
1.8k
Forks
480
Merge moyen
1 j 19 h
PR mergées (30 j)
285

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de clerk/javascript

Toutes les issues de clerk/javascript

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.