Skip to content

Commit 7b573d2

Browse files
dorlugasigalCopilot
andcommitted
fix(auth): simplify auth flow — remove localStorage caching
Replace the over-engineered getConfig() with localStorage caching and retries with a straightforward two-step flow: 1. checkAuth() — if authenticated, done 2. If server unreachable, getConfig() (no auth) to check if password is even required — if not, grant access No caching, no retries, no localStorage. Fixes false login page on mobile when DevTunnel auth cookie has not propagated to fetch yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ad65f32 commit 7b573d2

2 files changed

Lines changed: 38 additions & 52 deletions

File tree

src/frontend/src/hooks/useAuth.ts

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,11 @@ export function useAuth(): UseAuthReturn {
1818
let cancelled = false;
1919

2020
async function init() {
21-
// Determine if password is required (uses localStorage cache when server unreachable)
22-
const config = await getConfig();
23-
if (cancelled) return;
24-
setPasswordRequired(config.passwordRequired);
25-
26-
// No-password mode: skip auth checks entirely — always grant access
27-
if (!config.passwordRequired) {
28-
setAuthenticated(true);
29-
return;
30-
}
31-
32-
// Check for one-time-token in URL
21+
// Check for one-time-token in URL first
3322
const params = new URLSearchParams(window.location.search);
3423
const ott = params.get('ott');
3524

3625
if (ott) {
37-
// Remove ott from URL without reload
3826
params.delete('ott');
3927
const search = params.toString();
4028
const newUrl =
@@ -59,17 +47,37 @@ export function useAuth(): UseAuthReturn {
5947
}
6048
}
6149

62-
try {
63-
const { authenticated: isAuth, serverReachable } = await checkAuth();
64-
if (!cancelled) {
65-
if (!isAuth && !serverReachable) {
66-
setAuthenticated(false);
67-
return;
68-
}
69-
setAuthenticated(isAuth);
70-
}
71-
} catch {
72-
if (!cancelled) setAuthenticated(false);
50+
// Primary check: can we reach the server and are we authenticated?
51+
const { authenticated: isAuth, serverReachable } = await checkAuth();
52+
if (cancelled) return;
53+
54+
if (isAuth) {
55+
setAuthenticated(true);
56+
return;
57+
}
58+
59+
// Not authenticated — but is a password even required?
60+
// If server is reachable and returned 401, password is required.
61+
// If server is unreachable (tunnel stale, network down), check /api/config.
62+
if (serverReachable) {
63+
// Server responded with 401 — password is required
64+
setPasswordRequired(true);
65+
setAuthenticated(false);
66+
return;
67+
}
68+
69+
// Server unreachable — check if password is configured.
70+
// /api/config has no auth middleware, so it works even without a token.
71+
const config = await getConfig();
72+
if (cancelled) return;
73+
setPasswordRequired(config.passwordRequired);
74+
75+
if (!config.passwordRequired) {
76+
// No password mode + server unreachable (tunnel flaky) — grant access.
77+
// The terminal/sessions hub will show its own connection banner.
78+
setAuthenticated(true);
79+
} else {
80+
setAuthenticated(false);
7381
}
7482
}
7583

@@ -81,24 +89,12 @@ export function useAuth(): UseAuthReturn {
8189

8290
// Re-check auth when returning from background (e.g. mobile tab switch after hours idle).
8391
useEffect(() => {
84-
let retryTimer: ReturnType<typeof setTimeout> | null = null;
85-
8692
function handleVisibility() {
8793
if (document.hidden) return;
8894

8995
if (!passwordRequired) {
90-
// No-password mode: server is always "authenticated", but verify reachability.
91-
// If unreachable, keep authenticated=true — the terminal/sessions hub will
92-
// show its own connection banner. Once reachable again, everything auto-recovers.
93-
checkAuth().then(({ serverReachable }) => {
94-
if (!serverReachable && retryTimer === null) {
95-
// Schedule a silent retry in case tunnel just needs a moment
96-
retryTimer = setTimeout(() => {
97-
retryTimer = null;
98-
checkAuth(); // fire-and-forget; UI stays on terminal
99-
}, 5000);
100-
}
101-
});
96+
// No-password mode: never flip to unauthenticated — the terminal handles
97+
// connection issues with its own reconnect banner.
10298
return;
10399
}
104100

@@ -110,10 +106,7 @@ export function useAuth(): UseAuthReturn {
110106
}
111107

112108
document.addEventListener('visibilitychange', handleVisibility);
113-
return () => {
114-
document.removeEventListener('visibilitychange', handleVisibility);
115-
if (retryTimer !== null) clearTimeout(retryTimer);
116-
};
109+
return () => document.removeEventListener('visibilitychange', handleVisibility);
117110
}, [authenticated, passwordRequired]);
118111

119112
const login = useCallback(async (password: string): Promise<boolean> => {

src/frontend/src/services/api.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,11 @@ export async function getConfig(): Promise<{ passwordRequired: boolean }> {
186186
const res = await fetchWithTimeout(`${BASE}/api/config`, { credentials: 'same-origin' });
187187
const ct = res.headers.get('content-type') || '';
188188
if (!ct.includes('application/json')) {
189-
// Non-JSON response (e.g. DevTunnel auth HTML) — use cached value if available.
190-
// Default to passwordRequired=true (safe: shows login rather than bypassing auth).
191-
const cached = localStorage.getItem('tb:passwordRequired');
192-
return { passwordRequired: cached === null ? true : cached !== 'false' };
189+
// Non-JSON response (DevTunnel auth page, etc.) — assume password required (safe default)
190+
return { passwordRequired: true };
193191
}
194-
const data = (await res.json()) as { passwordRequired: boolean };
195-
localStorage.setItem('tb:passwordRequired', String(data.passwordRequired));
196-
return data;
192+
return (await res.json()) as { passwordRequired: boolean };
197193
} catch {
198-
// Network error — server may be starting up, SW race, or genuinely down.
199-
// Always default to passwordRequired=true (safe default) to prevent a stale
200-
// no-password cache from bypassing auth on a password-protected server.
201194
return { passwordRequired: true };
202195
}
203196
}

0 commit comments

Comments
 (0)