Eridu Services - Frontend Authentication & Authorization Skill
Provides guidance for implementing frontend authentication and authorization patterns in Eridu Services (React apps: erify_creators, erify_studios, eridu_auth UI).
Frontend Responsibilities
- •Collect credentials - Login form, SSO flow, password input
- •Store credentials securely - Token management, session storage
- •Send credentials with requests - Attach token to API calls
- •Manage user context - Keep track of who is logged in
- •Protect routes - Prevent access to pages user shouldn't see
- •Handle expiration - Refresh tokens, redirect on logout
Core Principles
Secure Token Storage
Rule: Don't store sensitive tokens in unsafe locations
Options by security level:
- •
Most Secure - HTTP-only Cookies
- •✅ Token cannot be accessed by JavaScript
- •✅ Automatically sent with requests
- •✅ Protected against XSS attacks
- •Use for: Production environments, sensitive apps
- •
Good - Secure Session Storage
- •✅ Token cleared when tab closes
- •✅ Cannot be accessed by other sites
- •⚠️ Can be accessed by other tabs/scripts on same site
- •Use for: Medium-security apps, internal tools
- •
Poor - localStorage
- •❌ Accessible to any JavaScript on the page
- •❌ Persists after browser close
- •❌ Vulnerable to XSS attacks
- •Avoid if: Storing authentication tokens
Never:
- •❌ Store tokens in URL
- •❌ Store tokens in plaintext
- •❌ Log tokens to console
- •❌ Display tokens in UI
Validate on Every Render
Rule: Don't assume user is authenticated; check on mount
// ✅ CORRECT: Check auth on mount
useEffect(() => {
const token = authStorage.getToken();
if (!token) {
navigate('/login');
}
}, []);
// ❌ WRONG: Assume auth persists
const user = useAuth(); // Might be null!
Handle Expiration Gracefully
Rule: Refresh tokens before expiration; redirect on failure
// ✅ Refresh before expiration
const expiresIn = parseJwt(token).exp - Date.now();
const refreshTime = expiresIn - 5 * 60 * 1000; // 5 min before
setTimeout(() => {
authService.refresh().catch(() => {
// Refresh failed - log out
handleLogout();
});
}, refreshTime);
// ❌ Let it expire
// Don't ignore token expiration - leads to 401 errors
Protect All Sensitive Routes
Rule: Require authentication before showing sensitive pages
// ✅ CORRECT: Redirect to login if not authenticated
<Route
path="/admin"
element={<ProtectedRoute><AdminPage /></ProtectedRoute>}
/>
// ❌ WRONG: Show page then check auth
<Route path="/admin" element={<AdminPage />} />
// (AdminPage would handle auth check - bad UX)
Implementation Patterns
Login Flow
async function handleLogin(email: string, password: string) {
try {
// 1. Call login endpoint
const response = await apiClient.post('/auth/login', {
email,
password,
});
const { token, user } = response.data;
// 2. Store token securely
authStorage.setToken(token);
// 3. Update app state
setUser(user);
setIsAuthenticated(true);
// 4. Navigate to authenticated area
navigate('/dashboard');
} catch (error) {
// 5. Show user-friendly error
setError('Invalid email or password');
}
}
Checklist:
- • Login endpoint validates credentials on backend
- • Token is stored securely (not in state only)
- • User context is updated after successful login
- • Errors don't reveal sensitive information
- • Loading state prevents double-submission
- • Redirect happens after state is set
Protected Routes
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
// Handle loading state (checking auth on mount)
if (isLoading) {
return <LoadingSpinner />;
}
// Redirect unauthenticated users
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
}
// Usage
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
Checklist:
- • Loading state is handled during auth check
- • Unauthenticated users are redirected
- • Redirect uses
replaceto prevent back button issues - • Protected page is not rendered until auth confirmed
- • Loading spinner shown while checking auth
User Context Provider
// Create context
export const AuthContext = createContext<AuthContextType | null>(null);
// Provider component
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check auth on mount
useEffect(() => {
const checkAuth = async () => {
try {
const token = authStorage.getToken();
if (token) {
// Validate token with backend
const response = await apiClient.get('/auth/me');
setUser(response.data);
}
} catch (error) {
// Token invalid - clear it
authStorage.clearToken();
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
return (
<AuthContext.Provider value={{ user, setUser, isLoading }}>
{children}
</AuthContext.Provider>
);
}
// Hook to use context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Checklist:
- • Auth is checked on app mount
- • User context is available app-wide
- • Loading state prevents race conditions
- • Invalid tokens are cleared
- • Error during auth check is handled gracefully
Logout
async function handleLogout() {
try {
// 1. Notify backend (optional)
await apiClient.post('/auth/logout');
} catch (error) {
// Proceed even if backend fails
console.warn('Logout notification failed', error);
} finally {
// 2. Clear stored token
authStorage.clearToken();
// 3. Clear user context
setUser(null);
setIsAuthenticated(false);
// 4. Redirect to login
navigate('/login', { replace: true });
}
}
Checklist:
- • Backend is notified (if implementing logout endpoint)
- • Token is cleared from storage
- • User context is cleared
- • User is redirected to login
- • Logout completes even if backend fails
Token Refresh
function useTokenRefresh() {
const { isAuthenticated } = useAuth();
useEffect(() => {
if (!isAuthenticated) return;
// Calculate refresh interval (e.g., refresh 5 min before expiry)
const token = authStorage.getToken();
const decoded = parseJwt(token);
const expiresIn = decoded.exp * 1000 - Date.now();
const refreshTime = expiresIn - 5 * 60 * 1000; // 5 min before expiry
const timeoutId = setTimeout(async () => {
try {
const response = await apiClient.post('/auth/refresh');
const { token: newToken } = response.data;
authStorage.setToken(newToken);
} catch (error) {
// Refresh failed - log out
handleLogout();
}
}, Math.max(refreshTime, 0));
return () => clearTimeout(timeoutId);
}, [isAuthenticated]);
}
Checklist:
- • Refresh happens before token expiration
- • New token is stored securely
- • Failed refresh logs out user
- • Cleanup prevents memory leaks
- • Refresh is silent (no page reload)
API Request Interceptor
// Attach token to every API request
apiClient.interceptors.request.use((config) => {
const token = authStorage.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle 401 responses (token expired or invalid)
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Token is invalid - log out
handleLogout();
}
return Promise.reject(error);
}
);
Checklist:
- • Token is attached to all requests
- • 401 responses trigger logout
- • 403 responses don't trigger logout
- • Interceptor doesn't interfere with non-auth requests
- • Errors are properly propagated
Frontend Implementation Checklist
Token Management:
- • Token is stored securely (HTTP-only cookie preferred)
- • Token is not stored in state only
- • Token is not logged or exposed in console
- • Token is sent with every authenticated request
- • Token expiration is handled before it expires
- • Failed refresh redirects to login
- • Logout clears token from all storage
Route Protection:
- • Protected routes redirect to login if not authenticated
- • Loading state is shown while checking auth
- • Redirect prevents unauthorized page flash
- • Public routes are not unnecessarily protected
- • Back button doesn't bypass protection
User Context:
- • User context is available app-wide
- • Auth is checked on app mount
- • User state is synchronized across tabs (optional but good)
- • Invalid tokens are cleared automatically
- • User is available in protected components
Logout:
- • Logout clears all auth data
- • Logout navigates to login page
- • Logout completes even if backend fails
- • User cannot access protected pages after logout
- • Tokens are removed from all storage
Error Handling:
- • Login errors don't expose user enumeration
- • Auth errors are user-friendly
- • Failed requests don't expose tokens
- • 401 responses trigger logout
- • 403 responses show permission error
Security:
- • No hardcoded credentials
- • No credentials in URLs
- • HTTPS is used for all auth requests
- • Token is validated on backend for every request
- • Session/token lifetime is reasonable
Related Skills
- •authentication-authorization-backend/SKILL.md - Backend token validation
- •authentication-authorization/SKILL.md - General principles
- •controller-pattern/SKILL.md - Handling authenticated requests
- •data-validation/SKILL.md - Input validation for login forms
Common Patterns by Framework
React + TanStack Query
// Fetch user with auth
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: async () => {
const response = await apiClient.get('/auth/me');
return response.data;
},
retry: 1,
staleTime: 10 * 60 * 1000, // 10 minutes
});
React + Context
// See User Context Provider section above
React Router v6
// Use ProtectedRoute component for sensitive pages // See Protected Routes section above
Troubleshooting
User gets logged out unexpectedly:
- •Check token expiration time
- •Verify refresh is happening before expiration
- •Check if token is being cleared unexpectedly
- •Look for logout being called without user action
Infinite redirect loop:
- •Check ProtectedRoute doesn't redirect to itself
- •Verify loading state prevents premature redirects
- •Check auth check doesn't trigger during login
Token doesn't attach to requests:
- •Verify interceptor is configured
- •Check token exists in storage
- •Verify endpoint matches interceptor pattern
- •Look for request being made before token is loaded
Other tabs/windows out of sync:
- •Implement cross-tab communication (storage event listener)
- •Or use cookie-based auth (shared across tabs automatically)
- •Or refresh user context periodically