Skip to content

[Mobile] Handle device-clock skew for backend JWT auth (currently anonymous fallback) #930

Description

@piyalbasu

Context

PR #925 wires the per-request backend JWT into apiFactory. buildAuthJwt derives iat/exp from the device clock (Date.now()), and freighter-backend-v2 validates timing with:

  • MaxTokenLifetime = 15s (rejects exp - iat > 15s)
  • ClockSkewLeeway = 5s (applied to the future-iat bound, the future-exp bound, and the WithLeeway expiry check)

Net client-clock tolerance is asymmetric: ~+5s ahead (future-iat check) / ~−20s behind (15s lifetime + 5s leeway). A device clock off beyond that fails all authenticated V2 requests while the wallet is unlocked, and the 401-retry re-signs from the same bad clock so it fails identically. (Reported by @aristidesstaffieri on #925.)

Current behavior (#925)

None — #925 matches the extension's authedFetch: retry once on 401, then the 401 surfaces to the caller. A skewed-clock user's authenticated V2 requests fail while the wallet is unlocked (they still work while locked, since locked requests are anonymous — which makes it confusing to debug from reports). We considered a client-side anonymous fallback but dropped it: it broke parity with the extension and masked genuine auth failures behind a permissive-backend success. This issue tracks the real fix.

Signal

Backend 401 / ExpiredTokenError / ReasonBadTiming rate on the v2 auth middleware (and/or mobile Sentry ApiError{status:401} on v2 calls). If a meaningful cohort is failing on timing, act.

Decision tree

  1. Small, common skew → widen the backend ClockSkewLeeway (one constant in freighter-backend-v2 internal/auth/claims.go). Use ClockSkewLeeway, NOT MaxTokenLifetime — leeway feeds all three timing bounds so it absorbs skew both directions, whereas raising the lifetime only helps behind-skew and lets clients mint longer-lived (more-replayable) tokens. Cost: the replay window widens ~linearly with leeway; acceptable up to tens of seconds since tokens are bound to sub+methodAndPath+bodyHash. This is the same lever available to the extension, so it fixes both clients at once.
  2. Long tail of large skew (can't leeway past minutes without gutting replay defense) → client-side clock sync: capture the offset from a response Date header (or a dedicated time endpoint) and sign iat/exp in server time. (Precedent: AWS SDK clock-skew correction reads Date off the error response and retries.) Would apply to both mobile and the extension.

Acceptance

  • Decide 1 vs 2 from real 401/timing-rejection data.
  • If (1): PR to widen ClockSkewLeeway + note the replay-window trade in the design doc.
  • If (2): client Date/time-sync (mobile + extension).

Refs: PR #925, review thread #925 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions