Skip to content

fix refresh auth token#1240

Merged
ebma merged 6 commits into
stagingfrom
fix/session-token-refresh
Jul 3, 2026
Merged

fix refresh auth token#1240
ebma merged 6 commits into
stagingfrom
fix/session-token-refresh

Conversation

@Sharqiewicz

Copy link
Copy Markdown
Member

Problem it fixes: Users frequently had to log out and back in before they could make requests. Root cause: the Supabase access token expires (~1h) and the frontend's token-refresh was unreliable - it either
didn't fire in time or, when a refresh failed, it wiped the whole session and forced re-login.


Background

  • The "never expires" Supabase setting applies to the refresh token / session (Time-box = 0, Inactivity = 0), not the access token. The access token is short-lived by design and cannot/should not be made
    "forever" (Supabase caps it at 1 week; access JWTs aren't individually revocable).
  • So the real issue was never token lifetime - it was the refresh mechanism. This PR fixes that.

Key decisions

  1. Fix the refresh, not the token lifetime. We did not raise the access-token TTL. Short access token + reliable refresh is the correct model.
  2. Refresh goes through the backend (POST /v1/auth/refresh), not the browser calling Supabase directly. Avoids the browser racing Supabase's refresh-token rotation and trims client attack surface. (No
    backend change — the endpoint already existed.)
  3. Fail-open on transient errors, fail-closed only on confirmed-invalid. A network blip / timeout / 5xx must not log the user out. Only a real 401 from /auth/refresh (revoked/invalid refresh token) ends
    the session.
  4. One refresher, at the app root, driven by the JWT's real expiry - not a hardcoded interval duplicated across pages.
  5. The app owns tokens; the Supabase client is passive. We turned off the Supabase client's own background refresh so it can't compete with ours (single source of truth).

What was done

Change Detail File(s)
401 → refresh → retry On a 401, the API client refreshes once (single-flight, shared across concurrent requests) and retries. Expired-token requests now self-heal. services/api/api-client.ts
Don't nuke session on transient failures Refresh clears the session only on a confirmed 401; transient failures are retried with the session intact. services/auth.ts, machines/ramp.actors.ts
Single app-root refresher off JWT exp Schedules a refresh 60s before real expiry, reschedules off each new token, retries transient failures. Replaced the fragile 55-min setInterval that was
duplicated across pages and torn down on navigation. contexts/rampState.tsx, hooks/useAuthTokens.ts
Backend refresh transport Frontend refresh now calls /v1/auth/refresh instead of supabase.auth.refreshSession with the anon key. services/auth.ts
Passive Supabase client autoRefreshToken, persistSession, detectSessionInUrlfalse. config/supabase.ts
Expiry-aware helpers isAuthenticated() now checks expiry; added getAccessTokenExpiryMs() + JWT decode. services/auth.ts
Security spec updated Flow step, one invariant (session teardown only on confirmed-invalid refresh), one audit item. docs/security-spec/01-auth/supabase-otp.md

How it fixes both failure modes:

  • Refresh never fired → app-root scheduler always fires off real exp; any request that still hits an expired token recovers via the 401 retry.
  • Refresh fired but failed → logout → transient failures no longer clear the session; only a real 401 logs out (cleanly routing to the login screen).

@netlify

netlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploy Preview for vortex-sandbox ready!

Name Link
🔨 Latest commit 456df09
🔍 Latest deploy log https://app.netlify.com/projects/vortex-sandbox/deploys/6a47816deefc320008618aae
😎 Deploy Preview https://deploy-preview-1240--vortex-sandbox.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploy Preview for vortexfi ready!

Name Link
🔨 Latest commit 456df09
🔍 Latest deploy log https://app.netlify.com/projects/vortexfi/deploys/6a47816d9e1a09000876e78d
😎 Deploy Preview https://deploy-preview-1240--vortexfi.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves frontend authentication reliability by making access-token refresh deterministic and resilient, preventing users from being forced to log out due to expired access tokens or transient refresh failures. It centralizes refresh behavior (scheduler + 401 refresh/retry), routes refresh via the backend, and disables Supabase client background refresh to avoid competing refresh flows.

Changes:

  • Implemented single-flight refresh and one retry on 401 in the API client.
  • Moved refresh transport to POST /v1/auth/refresh and changed refresh error handling to only clear session on confirmed-invalid (401).
  • Added an app-root token refresh scheduler driven by JWT exp, and made the Supabase client passive (no auto-refresh/persist).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/security-spec/01-auth/supabase-otp.md Documents the new refresh flow and the “teardown only on confirmed-invalid refresh” invariant.
apps/frontend/src/services/auth.ts Adds expiry-aware auth helpers and refresh via backend /v1/auth/refresh.
apps/frontend/src/services/api/api-client.ts Adds single-flight refresh-on-401 with one retry using the refreshed token.
apps/frontend/src/machines/ramp.actors.ts Adjusts token refresh actor behavior to preserve session on transient failures.
apps/frontend/src/hooks/useAuthTokens.ts Removes the old interval-based refresh setup hook.
apps/frontend/src/contexts/rampState.tsx Adds a single app-root refresh scheduler based on decoded JWT expiry.
apps/frontend/src/config/supabase.ts Disables Supabase client auto-refresh/persist to keep it passive.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/frontend/src/services/auth.ts
Comment thread apps/frontend/src/contexts/rampState.tsx Outdated
Comment thread apps/frontend/src/machines/ramp.actors.ts Outdated
ebma added 4 commits July 3, 2026 10:59
atob can throw on unpadded base64url payloads; convert to base64 and
re-pad so access-token expiry decoding is reliable.

Addresses PR #1240 review feedback.
If getAccessTokenExpiryMs() returns null the scheduler previously exited
without rescheduling, permanently disabling the app-root refresh loop.
Retry after a short delay so a later decodable token re-establishes it.

Addresses PR #1240 review feedback.
refreshAccessToken() already clears the stored session on a confirmed
401, so clearing again in checkAndRefreshTokenActor duplicated the
teardown. Keep session-teardown ownership in one place.

Addresses PR #1240 review feedback.
/v1/auth/refresh previously caught every error and returned 401, so a
Supabase outage or 5xx looked identical to a revoked refresh token and
forced users to log out — defeating the frontend's fail-open design.

Classify the Supabase error: a definite 4xx auth error means the refresh
token is invalid (401); retryable-fetch/5xx/transport errors and any
unexpected error return 503 so the frontend keeps the session and
retries. Update the security spec's invariant, flow, and audit item.

Addresses the transient-failure gap in PR #1240.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread apps/frontend/src/services/auth.ts Outdated
Comment thread apps/frontend/src/contexts/rampState.tsx
@Sharqiewicz Sharqiewicz requested a review from ebma July 3, 2026 09:17
- isAuthenticated() decodes expiry from the token already in scope
  instead of reading localStorage a second time via getAccessTokenExpiryMs().
- Guard scheduleNext() on the cancelled flag so a refresh callback that is
  already running at unmount can't schedule a new timer after cleanup.

Addresses PR #1240 review feedback (review 4624527032).
@ebma ebma merged commit 4bdcabf into staging Jul 3, 2026
6 of 7 checks passed
@ebma ebma deleted the fix/session-token-refresh branch July 3, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants