In different sectors of our space station, we need different access levels. Not every crew member should have access to all areas - similarly in web applications, where some pages should be accessible only to logged-in users or people with specific permissions.
Protected routes are a mechanism that allows restricting access to certain parts of the application only to authorized users. They are usually implemented by checking whether the user is logged in or has the appropriate permissions.
The simplest way to protect a route is by using a higher-order component (HOC) that checks the authentication state:
1import { Navigate, useLocation } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3
4function ProtectedRoute({ children }) {
5 const { user, isLoading } = useAuth();
6 const location = useLocation();
7
8 // Show loading indicator while checking authentication state
9 if (isLoading) {
10 return <div className="loading">Checking section permissions...</div>;
11 }
12
13 // If the user is not logged in, redirect to the login page
14 if (!user) {
15 // Save the path the user came from so we can return after login
16 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
17 }
18
19 // If the user is logged in, render the protected content
20 return children;
21}Then we can use this component in our routes:
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route path="/login" element={<LoginPage />} />
4
5 {/* Protected routes */}
6 <Route path="/command-center" element={
7 <ProtectedRoute>
8 <CommandCenter />
9 </ProtectedRoute>
10 } />
11
12 <Route path="/restricted-area" element={
13 <ProtectedRoute>
14 <RestrictedArea />
15 </ProtectedRoute>
16 } />
17</Routes>For the above code to work, we need an authentication context that will store information about the logged-in user:
1import { createContext, useContext, useState, useEffect } from 'react';
2
3const AuthContext = createContext(null);
4
5export function AuthProvider({ children }) {
6 const [user, setUser] = useState(null);
7 const [isLoading, setIsLoading] = useState(true);
8
9 useEffect(() => {
10 // Check if the user is already logged in (e.g., via a token in localStorage)
11 const checkAuthStatus = async () => {
12 try {
13 setIsLoading(true);
14 const token = localStorage.getItem('authToken');
15
16 if (token) {
17 // Token validation on the server or JWT decoding
18 const userData = await validateToken(token); // function implemented separately
19 setUser(userData);
20 }
21 } catch (error) {
22 console.error('Authentication error:', error);
23 // In case of error, log out the user
24 localStorage.removeItem('authToken');
25 setUser(null);
26 } finally {
27 setIsLoading(false);
28 }
29 };
30
31 checkAuthStatus();
32 }, []);
33
34 // Login and logout functions
35 const login = async (credentials) => {
36 setIsLoading(true);
37 try {
38 // Call login API
39 const response = await apiLogin(credentials);
40 const { token, user: userData } = response;
41
42 // Save the token
43 localStorage.setItem('authToken', token);
44 setUser(userData);
45 return true;
46 } catch (error) {
47 console.error('Login error:', error);
48 return false;
49 } finally {
50 setIsLoading(false);
51 }
52 };
53
54 const logout = () => {
55 localStorage.removeItem('authToken');
56 setUser(null);
57 };
58
59 return (
60 <AuthContext.Provider value={{ user, isLoading, login, logout }}>
61 {children}
62 </AuthContext.Provider>
63 );
64}
65
66// Hook for easy access to the authentication context
67export function useAuth() {
68 return useContext(AuthContext);
69}On the login page, we can use information about the previous path to redirect the user back after logging in:
1import { useLocation, useNavigate } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3import { useState } from 'react';
4
5function LoginPage() {
6 const { login } = useAuth();
7 const navigate = useNavigate();
8 const location = useLocation();
9
10 // Get the path the user was redirected from (if it exists)
11 const from = location.state?.from || '/command-center';
12
13 const [credentials, setCredentials] = useState({
14 username: '',
15 password: ''
16 });
17
18 const handleChange = (e) => {
19 const { name, value } = e.target;
20 setCredentials(prev => ({
21 ...prev,
22 [name]: value
23 }));
24 };
25
26 const handleSubmit = async (e) => {
27 e.preventDefault();
28
29 const success = await login(credentials);
30
31 if (success) {
32 // Redirect back to the protected path
33 navigate(from, { replace: true });
34 }
35 };
36
37 return (
38 <div className="login-container">
39 <h1>Space Station Access Center</h1>
40 <form onSubmit={handleSubmit}>
41 <div className="form-group">
42 <label htmlFor="username">Astronaut ID:</label>
43 <input
44 id="username"
45 name="username"
46 type="text"
47 value={credentials.username}
48 onChange={handleChange}
49 required
50 />
51 </div>
52 <div className="form-group">
53 <label htmlFor="password">Access Code:</label>
54 <input
55 id="password"
56 name="password"
57 type="password"
58 value={credentials.password}
59 onChange={handleChange}
60 required
61 />
62 </div>
63 <button type="submit" className="login-button">
64 Gain Access
65 </button>
66 </form>
67 </div>
68 );
69}We often need more complex logic than just checking if the user is logged in. For example, it may be necessary to check if the user has the appropriate role:
1function RoleProtectedRoute({ children, requiredRoles = [] }) {
2 const { user, isLoading } = useAuth();
3 const location = useLocation();
4
5 if (isLoading) {
6 return <div className="loading">Checking permissions...</div>;
7 }
8
9 // Check if the user is logged in
10 if (!user) {
11 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
12 }
13
14 // Check if the user has the required role
15 const hasRequiredRole = requiredRoles.length === 0 ||
16 requiredRoles.some(role => user.roles.includes(role));
17
18 if (!hasRequiredRole) {
19 return <Navigate to="/unauthorized" replace />;
20 }
21
22 return children;
23}
24
25// Usage:
26<Route
27 path="/engine-room"
28 element={
29 <RoleProtectedRoute requiredRoles={['engineer', 'captain']}>
30 <EngineRoom />
31 </RoleProtectedRoute>
32 }
33/>A good practice is to group all protected routes in one place, which makes them easier to manage:
1function AppRoutes() {
2 return (
3 <Routes>
4 {/* Public paths */}
5 <Route path="/" element={<HomePage />} />
6 <Route path="/login" element={<LoginPage />} />
7 <Route path="/register" element={<RegisterPage />} />
8 <Route path="/about" element={<AboutPage />} />
9
10 {/* Protected section */}
11 <Route path="/dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
12 <Route index element={<Overview />} />
13 <Route path="profile" element={<Profile />} />
14 <Route path="settings" element={<Settings />} />
15
16 {/* Routes for engineers */}
17 <Route path="maintenance" element={
18 <RoleProtectedRoute requiredRoles={['engineer']}>
19 <MaintenancePanel />
20 </RoleProtectedRoute>
21 } />
22
23 {/* Routes for captains */}
24 <Route path="command" element={
25 <RoleProtectedRoute requiredRoles={['captain']}>
26 <CommandCenter />
27 </RoleProtectedRoute>
28 } />
29 </Route>
30
31 {/* Error handling */}
32 <Route path="/unauthorized" element={<UnauthorizedPage />} />
33 <Route path="*" element={<NotFoundPage />} />
34 </Routes>
35 );
36}In the example above, we use nested routes where the main
DashboardLayout component is protected, and all routes nested within it are automatically protected. Additionally, some routes have extra role-based protections.We can combine route protection with lazy loading to improve application performance:
1import { lazy, Suspense } from 'react';
2
3// Lazy loading components
4const CommandCenter = lazy(() => import('./CommandCenter'));
5const MaintenancePanel = lazy(() => import('./MaintenancePanel'));
6
7// In routing:
8<Route path="command" element={
9 <RoleProtectedRoute requiredRoles={['captain']}>
10 <Suspense fallback={<Loading />}>
11 <CommandCenter />
12 </Suspense>
13 </RoleProtectedRoute>
14} />Sometimes a user may try to access multiple protected routes before logging in. We can remember more than one route:
1function useRedirectHistory() {
2 const [history, setHistory] = useState([]);
3
4 const addToHistory = (path) => {
5 setHistory(prev => {
6 // Avoid duplicates
7 if (!prev.includes(path)) {
8 return [...prev, path].slice(-5); // Keep only the last 5 paths
9 }
10 return prev;
11 });
12 };
13
14 const getLastPath = () => history[history.length - 1] || '/dashboard';
15
16 return { addToHistory, getLastPath };
17}An important aspect of route protection is handling session expiration:
1function useSessionExpiration() {
2 const { logout } = useAuth();
3 const navigate = useNavigate();
4
5 useEffect(() => {
6 // Handle JWT token expiration
7 const checkTokenExpiration = () => {
8 const token = localStorage.getItem('authToken');
9 if (token) {
10 try {
11 const payload = JSON.parse(atob(token.split('.')[1]));
12 const expirationTime = payload.exp * 1000; // Convert to milliseconds
13
14 if (Date.now() >= expirationTime) {
15 logout();
16 navigate('/login', {
17 state: {
18 message: 'Your session has expired. Please log in again.'
19 }
20 });
21 }
22 } catch (e) {
23 console.error('Token validation error:', e);
24 logout();
25 }
26 }
27 };
28
29 const interval = setInterval(checkTokenExpiration, 60000); // Check every minute
30 return () => clearInterval(interval);
31 }, [logout, navigate]);
32}Protecting routes in React Router is a key aspect of creating secure applications. The main techniques include:
Remember that client-side (browser) security should never be the only layer of protection. It should always be supplemented with proper server-side security, which should independently verify user permissions before providing access to protected resources.