Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#10,231 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
18/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
javascript, nextjs, typescript

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.

主要言語
TypeScript
スター
1.8k
フォーク
480
平均マージ
1日 21時間
マージ済み PR(30日)
308

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

clerk/javascript のほかの issue

clerk/javascript の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。