Make session-expiry checks tolerant of client clock skew (derive a server-time offset) #47158
Replies: 2 comments
-
|
Thanks for the detailed write-up - this does look like a real client-side robustness issue, not just a docs/UX problem around short JWT expiries. The proposed offset approach makes sense to me. If the client can derive “server now” from a freshly issued token or response metadata, then expiry checks should consistently compare against that adjusted time instead of raw A few implementation details seem important:
Using A configurable time source could still be useful for tests or advanced integrations, but I would not want that to be the only fix. The failure mode affects normal users who will not know they need to configure anything, so automatic skew compensation seems like the better default behavior. One subtle point: a plain This feels like a reasonable change to auth-js as long as it is centralized and covered with regression tests for clocks ahead/behind the server. A good test case would be: local clock ahead by more than the JWT lifetime, sign in succeeds, If my answer solved your problem, you can click answered the question. I'm really here to help, and along the way I'm also collecting Galaxy Brain badges haha 😆 |
Beta Was this translation helpful? Give feedback.
-
|
This is a well-researched writeup — and the root cause you've traced is accurate: auth-js evaluates expiry against A server-time-offset approach (derive skew from a server-provided timestamp — e.g. the The awkward part today is there's no clean injection point: Definitely worth filing on the auth-js repo (supabase/auth-js) in addition to here, given you've already localized it to specific functions and versions — that's most of the work a maintainer would need to scope a |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Problem
When a user's device clock is wrong, auth-js becomes unusable, and there's no
way for the client to compensate.
Every session-expiry decision compares the server-issued
expires_atagainst thelocal device clock (
Date.now()). If the device clock runs ahead by more thanroughly the token lifetime, each freshly-issued access token is judged "already
expired," so the client refreshes immediately, gets another token it also thinks
is expired, and loops — until the per-IP refresh rate limit returns
429and thesession wedges. To the user it just looks like "I can't log in," with no hint that
their clock is the cause.
We hit this in production: one user with a misconfigured OS clock generated
288 refresh tokens across 13 sessions in ~36 minutes, reproduced in two
browsers on the same machine. Resyncing their OS clock fixed it.
The User sessions docs already
acknowledge this ("user devices … can sometimes be off by minutes or even hours"),
but the only suggested mitigation is "don't use a short JWT expiry" — there's no
way to make the client itself robust to skew.
Root cause (auth-js 2.108.0)
Expiry is evaluated against
Date.now()in several spots, e.g.:GoTrueClient.__loadSession()—expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS(so even
getSession()triggers a refresh on a skewed clock)GoTrueClient._autoRefreshTokenTick()lib/helpers.tsexpiresAt()—Math.round(Date.now()/1000) + expiresInThere's no option to override the time source, and nothing derives an offset from
the server. (The library already detects skew in one place — it logs "issued in
the future? Check the device clock for skew" — but only warns.)
Proposed solution
Let the client compensate for a known offset. A freshly-minted token's
iatiseffectively the server's "now," so at each sign-in/refresh the client can compute
offset = localNow - iatand apply it wherever expiry is evaluated(
localNow - offsetinstead of rawlocalNow). With the offset applied, a wrongdevice clock no longer mis-triggers refresh.
Either form would work:
iat(or the responseDateheader) and use it internally in all expiry comparisons; or
clockToleranceoption inGoTrueClientOptions.Alternatives considered
autoRefreshToken: false+ reactive refresh — doesn't help:getSession()still refreshes against
Date.now()on reads.maintained monkeypatch against internal code.
Related (not duplicates)
Server. Same family but server-side skew (infra), not the user's device.
Touches expiry timing; not clock-skew tolerance.
verification clock skew. Clock-skew tolerance is a normal feature for an auth
SDK to offer.
Additional context
supabase-js / auth-js
2.108.0. Server-side validation already uses the serverclock, so a token the client thinks is "expired" is in fact still valid
server-side — the disagreement is purely the client's local-clock comparison,
which an offset resolves.
Beta Was this translation helpful? Give feedback.
All reactions