We use cookies to enhance your experience on the site
CodeWorlds

Protecting Routes in the Space Station

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.

What are Protected Routes?

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.

Basic Implementation of a Protected Route

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>

Authentication Context

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}

Redirect After Login

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}

Protecting Routes Based on Roles

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/>

Organizing the Protected Section of the Application

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.

Advanced Techniques

Lazy Loading for Protected Routes

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} />

Remembering Multiple Routes

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}

Handling Session Expiration

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}

Summary

Protecting routes in React Router is a key aspect of creating secure applications. The main techniques include:

  1. Creating higher-order components (HOC) to protect routes
  2. Using React Context to manage authentication state
  3. Redirecting users to the login page while remembering the original path
  4. Implementing role-based access control
  5. Organizing the application structure with a clear division into public and protected sections
  6. Handling session expiration and automatic logout

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.

Go to CodeWorlds