Skip to content

Commit e29245e

Browse files
committed
fix: prevent premature session expiry in extension
Three issues causing the extension session to appear expired: 1. Login endpoint didn't return access_token_expires_at, so the extension assumed the access token was always expired and triggered a refresh cycle on every single sync call. Now the Supabase JWT expiry is included in the login response. 2. ensureValidToken() made a network call to /api/auth/session on every sync even when the token wasn't expired. Any transient failure (timeout, network blip) would trigger an unnecessary refresh cascade. Now trusts the stored expiry and lets the actual API call handle 401s. 3. Fixed stale comments saying '1 year' when the actual session duration is 2 years (EXTENSION_SESSION_DURATION_MS).
1 parent 6528094 commit e29245e

3 files changed

Lines changed: 14 additions & 22 deletions

File tree

apps/extension/src/background/index.js

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -851,24 +851,12 @@ async function ensureValidToken() {
851851
return true;
852852
}
853853

854-
// Token should be valid based on expiry, but validate to be sure
855-
// Only do this if we haven't refreshed recently (avoid unnecessary network calls)
856-
const isValid = await validateToken();
857-
if (isValid) {
858-
return true;
859-
}
860-
861-
// Token validation failed despite not being expired - try refresh
862-
console.log('[MarkSyncr] Token validation failed unexpectedly, attempting refresh...');
863-
const refreshed = await tryRefreshToken();
864-
865-
if (!refreshed) {
866-
// Don't clear — preserve extension_token for future retry.
867-
// tryRefreshToken handles clearing on definitive 401 (session revoked/expired).
868-
console.log('[MarkSyncr] Token refresh failed, will retry on next attempt');
869-
return false;
870-
}
871-
854+
// Token is not expired based on stored expiry — trust it.
855+
// Don't make a network validation call on every sync; that wastes bandwidth
856+
// and can cause spurious failures (timeouts, network blips) that trigger
857+
// unnecessary refresh cascades. The actual API call in performSync will
858+
// naturally return 401 if the token is invalid, and apiRequest() already
859+
// handles that by calling tryRefreshToken() and retrying.
872860
return true;
873861
}
874862

apps/web/app/api/auth/extension/login/route.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export async function POST(request) {
103103
const extensionToken = generateSecureToken();
104104
const extensionTokenHash = hashToken(extensionToken);
105105

106-
// Step 3: Calculate expiration (1 year from now)
106+
// Step 3: Calculate expiration (2 years from now)
107107
const expiresAt = new Date(Date.now() + EXTENSION_SESSION_DURATION_MS);
108108

109109
// Step 4: Store extension session in database using admin client
@@ -145,6 +145,10 @@ export async function POST(request) {
145145
access_token: session.access_token,
146146
// Expiration of the extension session (not the access token)
147147
expires_at: sessionData.expires_at,
148+
// Expiration of the short-lived access token (typically 1 hour)
149+
// Without this, the extension assumes the token is expired on first use
150+
// and triggers an unnecessary refresh cycle on every sync
151+
access_token_expires_at: session.expires_at,
148152
// Session metadata
149153
session_id: sessionData.id,
150154
created_at: sessionData.created_at,

supabase/migrations/015_extension_sessions.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
--
44
-- This table stores long-lived session tokens for browser extensions.
55
-- Unlike Supabase's default JWT tokens (which expire in 1 hour with 7-day refresh tokens),
6-
-- extension sessions are designed to last 1 year to avoid requiring frequent re-logins.
6+
-- extension sessions are designed to last 2 years to avoid requiring frequent re-logins.
77
--
88
-- Security considerations:
99
-- - extension_token is a cryptographically secure random token (256 bits)
@@ -97,9 +97,9 @@ $$;
9797
GRANT EXECUTE ON FUNCTION cleanup_expired_extension_sessions() TO authenticated;
9898

9999
-- Comment on table for documentation
100-
COMMENT ON TABLE extension_sessions IS 'Long-lived session tokens for browser extensions (1 year expiry)';
100+
COMMENT ON TABLE extension_sessions IS 'Long-lived session tokens for browser extensions (2 year expiry)';
101101
COMMENT ON COLUMN extension_sessions.extension_token_hash IS 'SHA-256 hash of the extension token for secure verification';
102102
COMMENT ON COLUMN extension_sessions.supabase_refresh_token IS 'Supabase refresh token for obtaining new access tokens';
103103
COMMENT ON COLUMN extension_sessions.device_id IS 'Unique identifier for the browser/device';
104-
COMMENT ON COLUMN extension_sessions.expires_at IS 'Session expiration (default: 1 year from creation)';
104+
COMMENT ON COLUMN extension_sessions.expires_at IS 'Session expiration (default: 2 years from creation)';
105105
COMMENT ON COLUMN extension_sessions.last_used_at IS 'Last time the session was used (for activity tracking)';

0 commit comments

Comments
 (0)