[Frontend] Add Auth Guard on Page Reload
Summary
The frontend does not validate authentication state on page load/reload. When a user's session expires or authentication tokens are cleared, reloading the page should redirect to /login, but currently it does not.
Failing Test
- File:
tests/core/authentication.spec.ts
- Test:
should redirect to login when session expires
- Line: ~310
Steps to Reproduce
- Log in to the application
- Open browser dev tools
- Clear localStorage and cookies
- Reload the page
- Expected: Redirect to
/login
- Actual: Page remains on current route (e.g.,
/dashboard)
Research Findings
Auth Architecture Overview
Current Auth Flow
Page Load → AuthProvider.useEffect() → checkAuth()
│
┌──────────────┴──────────────┐
▼ ▼
localStorage.get() GET /auth/me
setAuthToken(stored) │
┌───────────┴───────────┐
▼ ▼
Success Error
setUser(data) setUser(null)
setAuthToken(null)
│ │
▼ ▼
isLoading=false isLoading=false
isAuthenticated=true isAuthenticated=false
Current Implementation (AuthContext.tsx lines 9-25)
useEffect(() => {
const checkAuth = async () => {
try {
const stored = localStorage.getItem('charon_auth_token');
if (stored) {
setAuthToken(stored);
}
const response = await client.get('/auth/me');
setUser(response.data);
} catch {
setAuthToken(null);
setUser(null);
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
RequireAuth Component (RequireAuth.tsx)
const RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingOverlay message="Authenticating..." />;
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
API Client 401 Handler (client.ts lines 23-31)
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
console.warn('Authentication failed:', error.config?.url);
}
return Promise.reject(error);
}
);
Root Cause Analysis
The existing implementation already handles this correctly!
Looking at the code flow:
- AuthProvider runs
checkAuth() on mount (useEffect with [])
- It calls
GET /auth/me to validate the session
- On error (401), it sets
user = null and isAuthenticated = false
- RequireAuth reads
isAuthenticated and redirects to /login if false
The issue is likely one of:
- Race condition:
RequireAuth renders before checkAuth() completes
- Token without validation: If token exists in localStorage but is invalid, the
GET /auth/me fails, but something may not be updating properly
- Caching issue:
isLoading may not be set correctly on certain paths
Verified Behavior
isLoading starts as true (line 8)
RequireAuth shows loading overlay while isLoading is true
checkAuth() sets isLoading=false in finally block
- If
/auth/me fails, user=null → isAuthenticated=false → redirect to /login
This should work! Need to verify with E2E test what's actually happening.
Potential Issues to Investigate
1. API Client Not Clearing Token on 401
The interceptor only logs, doesn't clear state:
if (error.response?.status === 401) {
console.warn('Authentication failed:', error.config?.url); // Just logs!
}
2. No Global Auth State Reset
When a 401 occurs on any API call (not just /auth/me), there's no mechanism to force logout.
3. localStorage Token Persists After Session Expiry
Backend sessions expire, but frontend keeps the localStorage token.
Recommended Solution
Option A: Enhanced API Interceptor (Minimal Change) ✅ RECOMMENDED
Modify api/client.ts to clear auth state on 401:
// Add global auth reset callback
let onAuthError: (() => void) | null = null;
export const setOnAuthError = (callback: (() => void) | null) => {
onAuthError = callback;
};
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
console.warn('Authentication failed:', error.config?.url);
localStorage.removeItem('charon_auth_token');
setAuthToken(null);
onAuthError?.(); // Trigger state reset
}
return Promise.reject(error);
}
);
Then in AuthContext.tsx, register the callback:
useEffect(() => {
setOnAuthError(() => {
setUser(null);
// Navigate will happen via RequireAuth
});
return () => setOnAuthError(null);
}, []);
Option B: Direct Window Navigation (Simpler)
In the 401 interceptor, redirect immediately:
if (error.response?.status === 401 && !error.config?.url?.includes('/auth/me')) {
localStorage.removeItem('charon_auth_token');
window.location.href = '/login';
}
Note: This causes a full page reload and loses SPA state.
Files to Modify
| File |
Change |
frontend/src/api/client.ts |
Add 401 handler with auth reset |
frontend/src/context/AuthContext.tsx |
Register auth error callback |
Implementation Checklist
Priority
Medium - Security improvement but not critical since API calls still require valid auth.
Labels
- frontend
- security
- auth
- enhancement
Related
- Fixes E2E test:
should redirect to login when session expires
- Part of Phase 1 E2E testing backlog
Auto-created from frontend-auth-guard-reload.md
[Frontend] Add Auth Guard on Page Reload
Summary
The frontend does not validate authentication state on page load/reload. When a user's session expires or authentication tokens are cleared, reloading the page should redirect to
/login, but currently it does not.Failing Test
tests/core/authentication.spec.tsshould redirect to login when session expiresSteps to Reproduce
/login/dashboard)Research Findings
Auth Architecture Overview
AuthProvider- manages user state, login/logout, token handlingUser,AuthContextType/loginif not authenticatedAuthProviderandRequireAuthCurrent Auth Flow
Current Implementation (AuthContext.tsx lines 9-25)
RequireAuth Component (RequireAuth.tsx)
API Client 401 Handler (client.ts lines 23-31)
Root Cause Analysis
The existing implementation already handles this correctly!
Looking at the code flow:
checkAuth()on mount (useEffectwith[])GET /auth/meto validate the sessionuser = nullandisAuthenticated = falseisAuthenticatedand redirects to/loginif falseThe issue is likely one of:
RequireAuthrenders beforecheckAuth()completesGET /auth/mefails, but something may not be updating properlyisLoadingmay not be set correctly on certain pathsVerified Behavior
isLoadingstarts astrue(line 8)RequireAuthshows loading overlay whileisLoadingis truecheckAuth()setsisLoading=falseinfinallyblock/auth/mefails,user=null→isAuthenticated=false→ redirect to/loginThis should work! Need to verify with E2E test what's actually happening.
Potential Issues to Investigate
1. API Client Not Clearing Token on 401
The interceptor only logs, doesn't clear state:
2. No Global Auth State Reset
When a 401 occurs on any API call (not just
/auth/me), there's no mechanism to force logout.3. localStorage Token Persists After Session Expiry
Backend sessions expire, but frontend keeps the localStorage token.
Recommended Solution
Option A: Enhanced API Interceptor (Minimal Change) ✅ RECOMMENDED
Modify api/client.ts to clear auth state on 401:
Then in AuthContext.tsx, register the callback:
Option B: Direct Window Navigation (Simpler)
In the 401 interceptor, redirect immediately:
Note: This causes a full page reload and loses SPA state.
Files to Modify
frontend/src/api/client.tsfrontend/src/context/AuthContext.tsxImplementation Checklist
api/client.tswith enhanced 401 interceptorAuthContext.tsxto register the callbackshould redirect to login when session expirespassesPriority
Medium - Security improvement but not critical since API calls still require valid auth.
Labels
Related
should redirect to login when session expiresAuto-created from frontend-auth-guard-reload.md