How would you implement authentication in a React SPA? Discuss token management, refresh strategies, and protected routes.
8 minadvancedreactsystem-designauthenticationspadiscuss
Quick Answer
Implementing authentication in a React SPA requires careful consideration of security, user experience, and token management.
Detailed Answer
How would you implement authentication in a React SPA? Discuss token management, refresh strategies, and protected routes.
Answer:
Implementing authentication in a React SPA requires careful consideration of security, user experience, and token management. Here's a comprehensive approach:
5.4.1. Authentication Architecture
Auth Context and Provider
interface User {
id: string;
email: string;
name: string;
role: 'user' | 'admin';
permissions: string[];
}
interface AuthState {
user: User | null;
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshAuthToken: () => Promise<void>;
hasPermission: (permission: string) => boolean;
hasRole: (role: string) => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<AuthState>({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: true,
error: null,
});
// Initialize auth state from localStorage
useEffect(() => {
const initializeAuth = async () => {
try {
const token = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (token && refreshToken) {
// Verify token and get user data
const user = await verifyToken(token);
setState(prev => ({
...prev,
user,
token,
refreshToken,
isAuthenticated: true,
isLoading: false,
}));
} else {
setState(prev => ({ ...prev, isLoading: false }));
}
} catch (error) {
// Clear invalid tokens
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setState(prev => ({ ...prev, isLoading: false }));
}
};
initializeAuth();
}, []);
const login = async (email: string, password: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const { user, accessToken, refreshToken } = await response.json();
// Store tokens securely
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setState({
user,
token: accessToken,
refreshToken,
isAuthenticated: true,
isLoading: false,
error: null,
});
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: error instanceof Error ? error.message : 'Login failed',
}));
throw error;
}
};
const logout = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setState({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
};
const refreshAuthToken = async () => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
logout();
return;
}
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const { accessToken, refreshToken: newRefreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
setState(prev => ({
...prev,
token: accessToken,
refreshToken: newRefreshToken,
}));
} catch (error) {
logout();
throw error;
}
};
const hasPermission = (permission: string): boolean => {
return state.user?.permissions.includes(permission) ?? false;
};
const hasRole = (role: string): boolean => {
return state.user?.role === role;
};
return (
<AuthContext.Provider
value={{
...state,
login,
logout,
refreshAuthToken,
hasPermission,
hasRole,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
5.4.2. Token Management
HTTP Client with Automatic Token Refresh
class ApiClient {
private baseURL: string;
private refreshPromise: Promise<string> | null = null;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
private async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = localStorage.getItem('accessToken');
const config: RequestInit = {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
};
let response = await fetch(`${this.baseURL}${endpoint}`, config);
// Handle token expiration
if (response.status === 401 && token) {
try {
const newToken = await this.refreshToken();
config.headers = {
...config.headers,
Authorization: `Bearer ${newToken}`,
};
response = await fetch(`${this.baseURL}${endpoint}`, config);
} catch (error) {
// Redirect to login
window.location.href = '/login';
throw error;
}
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
private async refreshToken(): Promise<string> {
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.performTokenRefresh();
try {
const newToken = await this.refreshPromise;
return newToken;
} finally {
this.refreshPromise = null;
}
}
private async performTokenRefresh(): Promise<string> {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.baseURL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const { accessToken, refreshToken: newRefreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
return accessToken;
}
// Public methods
async get<T>(endpoint: string): Promise<T> {
return this.makeRequest<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, data: any): Promise<T> {
return this.makeRequest<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
async put<T>(endpoint: string, data: any): Promise<T> {
return this.makeRequest<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.makeRequest<T>(endpoint, { method: 'DELETE' });
}
}
export const apiClient = new ApiClient(process.env.REACT_APP_API_URL || '');
5.4.3. Protected Routes
Route Protection Components
interface ProtectedRouteProps {
children: React.ReactNode;
requiredPermission?: string;
requiredRole?: string;
fallback?: React.ReactNode;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requiredPermission,
requiredRole,
fallback = <Navigate to="/login" replace />,
}) => {
const { isAuthenticated, user, isLoading } = useAuth();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated || !user) {
return <>{fallback}</>;
}
if (requiredRole && !user.role.includes(requiredRole)) {
return <Navigate to="/unauthorized" replace />;
}
if (requiredPermission && !user.permissions.includes(requiredPermission)) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
};
// Usage in routing
const AppRoutes = () => {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute requiredPermission="manage_settings">
<SettingsPage />
</ProtectedRoute>
}
/>
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
};
5.4.4. Advanced Security Features
Session Management
const useSessionManagement = () => {
const { logout } = useAuth();
const [lastActivity, setLastActivity] = useState(Date.now());
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
useEffect(() => {
const updateActivity = () => setLastActivity(Date.now());
// Track user activity
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
events.forEach(event => {
document.addEventListener(event, updateActivity, true);
});
// Check for session timeout
const checkSession = setInterval(() => {
if (Date.now() - lastActivity > SESSION_TIMEOUT) {
logout();
clearInterval(checkSession);
}
}, 60000); // Check every minute
return () => {
events.forEach(event => {
document.removeEventListener(event, updateActivity, true);
});
clearInterval(checkSession);
};
}, [lastActivity, logout]);
return { lastActivity };
};
Multi-Factor Authentication
interface MFAState {
isEnabled: boolean;
backupCodes: string[];
qrCode: string;
}
const useMFA = () => {
const [mfaState, setMfaState] = useState<MFAState | null>(null);
const { user } = useAuth();
const enableMFA = async () => {
try {
const response = await apiClient.post('/auth/mfa/setup', {});
setMfaState(response);
} catch (error) {
console.error('Failed to setup MFA:', error);
}
};
const verifyMFA = async (token: string) => {
try {
await apiClient.post('/auth/mfa/verify', { token });
return true;
} catch (error) {
return false;
}
};
const disableMFA = async (password: string) => {
try {
await apiClient.post('/auth/mfa/disable', { password });
setMfaState(null);
} catch (error) {
console.error('Failed to disable MFA:', error);
}
};
return {
mfaState,
enableMFA,
verifyMFA,
disableMFA,
};
};
5.4.5. Security Best Practices
Token Storage Security
// Secure token storage with encryption
class SecureStorage {
private static encrypt(data: string): string {
// Implement encryption logic
return btoa(data); // Simple base64 for demo
}
private static decrypt(encryptedData: string): string {
// Implement decryption logic
return atob(encryptedData); // Simple base64 for demo
}
static setItem(key: string, value: string): void {
const encrypted = this.encrypt(value);
localStorage.setItem(key, encrypted);
}
static getItem(key: string): string | null {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
try {
return this.decrypt(encrypted);
} catch {
return null;
}
}
static removeItem(key: string): void {
localStorage.removeItem(key);
}
}
// Use secure storage for tokens
SecureStorage.setItem('accessToken', token);
const token = SecureStorage.getItem('accessToken');
CSRF Protection
const useCSRFProtection = () => {
const [csrfToken, setCsrfToken] = useState<string | null>(null);
useEffect(() => {
const fetchCSRFToken = async () => {
try {
const response = await fetch('/api/csrf-token');
const { token } = await response.json();
setCsrfToken(token);
} catch (error) {
console.error('Failed to fetch CSRF token:', error);
}
};
fetchCSRFToken();
}, []);
const getHeaders = () => ({
'X-CSRF-Token': csrfToken,
});
return { csrfToken, getHeaders };
};
5.4.6. Error Handling and User Experience
Auth Error Boundary
class AuthErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
if (error.message.includes('401') || error.message.includes('403')) {
return { hasError: true };
}
return null;
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
if (error.message.includes('401')) {
// Redirect to login
window.location.href = '/login';
}
}
render() {
if (this.state.hasError) {
return <Navigate to="/login" replace />;
}
return this.props.children;
}
}
This comprehensive authentication system provides secure, user-friendly authentication with proper token management, role-based access control, and advanced security features.