The Syntax Diaries

Practical code notes, tools & guided learning for developers.

BlogToolsTutorialsAboutContact
PrivacyTermsCookiesAdmin

Š 2026 The Syntax Diaries

100% private ¡ no tracking ¡ works offline100% client-side/no data leaves your browser/no accounts/works offline

The Syntax Diaries
BlogToolsTutorialsAbout
build log live
Tutorial / React
Routing45 minbeginner

Protected Routes

Learn how to protect routes with authentication and authorization, creating secure navigation patterns in your React applications.

§ On this page

The Problem: Open AccessWhat Are Protected Routes?Building Authentication ContextYour First Protected RouteBuilding a Complete Authentication SystemSetting Up Protected Routes in Your AppAdvanced Protection Patterns1. Permission-Based Routes2. Conditional Route Protection3. Route Guards with Loading StatesPractice Exercise: Multi-Role DashboardSecurity Best Practices1. Never Trust Frontend-Only Protection2. Implement Token Refresh3. Handle Concurrent Auth ChecksWhat We've LearnedQuick Recap QuizWhat's Next?

Protected Routes#

Imagine you're building a house with different rooms. Some rooms like the living room are open to everyone, but others like your bedroom or safe need a key to enter. Protected routes work the same way in web applications - they ensure only authorized users can access certain pages.

Without protected routes, anyone could type /admin or /user-profile into their browser and access sensitive areas of your app. Protected routes are your app's security guards, checking credentials before allowing entry.

The Problem: Open Access#

By default, all React Router routes are accessible to everyone:

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/dashboard" element={<Dashboard />} />     {/* Anyone can access! */}
        <Route path="/admin" element={<AdminPanel />} />        {/* Anyone can access! */}
        <Route path="/profile" element={<UserProfile />} />     {/* Anyone can access! */}
      </Routes>
    </BrowserRouter>
  );
}

Security problems:

  • 🚨 Unauthorized access - Anyone can visit sensitive pages
  • 🚨 Data exposure - Personal information visible to anyone
  • 🚨 Admin functions exposed - Critical operations accessible to all
  • 🚨 Poor user experience - Users see errors instead of proper login prompts

What Are Protected Routes?#

Protected routes are React components that:

  • Check user authentication before rendering content
  • Redirect to login if user is not authenticated
  • Verify permissions for role-based access
  • Provide smooth user experience with proper loading and error states

Think of them as smart bouncers for your app!

Building Authentication Context#

First, let's create a system to manage user authentication state:

import React, { createContext, useContext, useState, useEffect } from 'react';

// Create authentication context
const AuthContext = createContext(null);

// Custom hook to use auth context
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

// Authentication provider component
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Check if user is logged in on app start
  useEffect(() => {
    const checkAuthStatus = async () => {
      try {
        // Check localStorage for saved auth token
        const token = localStorage.getItem('authToken');
        if (token) {
          // Verify token with server (simplified for demo)
          const userData = await verifyToken(token);
          setUser(userData);
        }
      } catch (error) {
        // Token invalid, clear it
        localStorage.removeItem('authToken');
        console.error('Auth check failed:', error);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuthStatus();
  }, []);
  
  // Login function
  const login = async (credentials) => {
    try {
      setLoading(true);
      
      // Call login API (simplified for demo)
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      });
      
      if (!response.ok) {
        throw new Error('Login failed');
      }
      
      const { user: userData, token } = await response.json();
      
      // Store token and user data
      localStorage.setItem('authToken', token);
      setUser(userData);
      
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    } finally {
      setLoading(false);
    }
  };
  
  // Logout function
  const logout = async () => {
    try {
      // Call logout API to invalidate token
      await fetch('/api/logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer \${localStorage.getItem('authToken')}`
        }
      });
    } catch (error) {
      console.error('Logout API failed:', error);
    } finally {
      // Always clear local state
      localStorage.removeItem('authToken');
      setUser(null);
    }
  };
  
  // Helper functions
  const isAuthenticated = () => !!user;
  const hasRole = (role) => user?.roles?.includes(role) || false;
  const hasPermission = (permission) => user?.permissions?.includes(permission) || false;
  
  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated,
    hasRole,
    hasPermission
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Simplified token verification (replace with real API call)
async function verifyToken(token) {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  // Mock user data (replace with real API response)
  return {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    roles: ['user'],
    permissions: ['read:profile', 'write:profile']
  };
}

Your First Protected Route#

Now let's create a component that protects routes:

import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthProvider';

function ProtectedRoute({ children, requireAuth = true, requiredRole = null, requiredPermission = null }) {
  const { user, loading, isAuthenticated, hasRole, hasPermission } = useAuth();
  const location = useLocation();
  
  // Show loading spinner while checking authentication
  if (loading) {
    return (
      <div style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '50vh',
        fontSize: '18px'
      }}>
        🔄 Checking authentication...
      </div>
    );
  }
  
  // Check authentication requirement
  if (requireAuth && !isAuthenticated()) {
    // Redirect to login with return URL
    return <Navigate to="/login" state={{ from: location.pathname }} replace />;
  }
  
  // Check role requirement
  if (requiredRole && !hasRole(requiredRole)) {
    return (
      <div style={{
        textAlign: 'center',
        padding: '50px',
        backgroundColor: '#ffebee',
        borderRadius: '8px',
        margin: '20px'
      }}>
        <h2>🚫 Access Denied</h2>
        <p>You don't have the required role ({requiredRole}) to access this page.</p>
        <p>Current roles: {user?.roles?.join(', ') || 'None'}</p>
      </div>
    );
  }
  
  // Check permission requirement
  if (requiredPermission && !hasPermission(requiredPermission)) {
    return (
      <div style={{
        textAlign: 'center',
        padding: '50px',
        backgroundColor: '#ffebee',
        borderRadius: '8px',
        margin: '20px'
      }}>
        <h2>🚫 Insufficient Permissions</h2>
        <p>You don't have permission ({requiredPermission}) to access this page.</p>
        <p>Current permissions: {user?.permissions?.join(', ') || 'None'}</p>
      </div>
    );
  }
  
  // User is authorized, render the protected content
  return children;
}

export default ProtectedRoute;

Building a Complete Authentication System#

Let's create a full authentication system with login, registration, and protected pages:

import React, { useState } from 'react';
import { useAuth } from './AuthProvider';
import { useNavigate, useLocation, Link } from 'react-router-dom';

// Login Page Component
function LoginPage() {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  // Get the page user was trying to access
  const from = location.state?.from || '/dashboard';
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setIsLoading(true);
    
    const result = await login(credentials);
    
    if (result.success) {
      // Redirect to intended page or dashboard
      navigate(from, { replace: true });
    } else {
      setError(result.error);
    }
    
    setIsLoading(false);
  };
  
  const handleChange = (e) => {
    setCredentials({
      ...credentials,
      [e.target.name]: e.target.value
    });
  };
  
  return (
    <div style={{
      maxWidth: '400px',
      margin: '50px auto',
      padding: '30px',
      border: '1px solid #ddd',
      borderRadius: '8px',
      backgroundColor: '#fff'
    }}>
      <h1 style={{ textAlign: 'center', marginBottom: '30px' }}>
        🔐 Login
      </h1>
      
      {location.state?.from && (
        <div style={{
          backgroundColor: '#fff3cd',
          color: '#856404',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px',
          textAlign: 'center'
        }}>
          Please log in to access {location.state.from}
        </div>
      )}
      
      {error && (
        <div style={{
          backgroundColor: '#ffebee',
          color: '#c62828',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px'
        }}>
          ❌ {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Email
          </label>
          <input
            type="email"
            name="email"
            value={credentials.email}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              fontSize: '16px'
            }}
            placeholder="Enter your email"
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Password
          </label>
          <input
            type="password"
            name="password"
            value={credentials.password}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              fontSize: '16px'
            }}
            placeholder="Enter your password"
          />
        </div>
        
        <button
          type="submit"
          disabled={isLoading}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isLoading ? '#ccc' : '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: isLoading ? 'not-allowed' : 'pointer'
          }}
        >
          {isLoading ? 'Logging in...' : 'Login'}
        </button>
      </form>
      
      <div style={{ marginTop: '20px', textAlign: 'center' }}>
        <p>Don't have an account?</p>
        <Link to="/register" style={{ color: '#0066cc' }}>
          Create one here
        </Link>
      </div>
      
      {/* Demo credentials */}
      <div style={{
        marginTop: '30px',
        padding: '15px',
        backgroundColor: '#f8f9fa',
        borderRadius: '4px',
        fontSize: '14px'
      }}>
        <strong>Demo Credentials:</strong>
        <br />
        Email: user@demo.com | Password: password (User role)
        <br />
        Email: admin@demo.com | Password: admin123 (Admin role)
      </div>
    </div>
  );
}

// User Dashboard (Protected)
function UserDashboard() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();
  
  const handleLogout = async () => {
    await logout();
    navigate('/');
  };
  
  return (
    <div style={{ padding: '20px' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #eee'
      }}>
        <div>
          <h1>👤 User Dashboard</h1>
          <p>Welcome back, {user?.name}!</p>
        </div>
        
        <button
          onClick={handleLogout}
          style={{
            padding: '10px 20px',
            backgroundColor: '#dc3545',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Logout
        </button>
      </header>
      
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
        gap: '20px'
      }}>
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>📊 Account Overview</h3>
          <p><strong>Email:</strong> {user?.email}</p>
          <p><strong>Role:</strong> {user?.roles?.join(', ')}</p>
          <p><strong>Member since:</strong> January 2024</p>
        </div>
        
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>🎯 Quick Actions</h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
            <Link to="/profile" style={{
              padding: '8px 16px',
              backgroundColor: '#0066cc',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px',
              textAlign: 'center'
            }}>
              Edit Profile
            </Link>
            <Link to="/settings" style={{
              padding: '8px 16px',
              backgroundColor: '#28a745',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px',
              textAlign: 'center'
            }}>
              Settings
            </Link>
          </div>
        </div>
        
        <div style={{
          padding: '20px',
          border: '1px solid #ddd',
          borderRadius: '8px',
          backgroundColor: '#f8f9fa'
        }}>
          <h3>📈 Recent Activity</h3>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ padding: '5px 0', borderBottom: '1px solid #eee' }}>
              ✅ Profile updated
            </li>
            <li style={{ padding: '5px 0', borderBottom: '1px solid #eee' }}>
              📝 New post created
            </li>
            <li style={{ padding: '5px 0' }}>
              🔑 Password changed
            </li>
          </ul>
        </div>
      </div>
    </div>
  );
}

// Admin Panel (Protected - Admin Only)
function AdminPanel() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();
  const [users, setUsers] = useState([
    { id: 1, name: 'John Doe', email: 'john@demo.com', role: 'user', status: 'active' },
    { id: 2, name: 'Jane Smith', email: 'jane@demo.com', role: 'user', status: 'active' },
    { id: 3, name: 'Admin User', email: 'admin@demo.com', role: 'admin', status: 'active' }
  ]);
  
  const handleLogout = async () => {
    await logout();
    navigate('/');
  };
  
  const toggleUserStatus = (userId) => {
    setUsers(prev => prev.map(user => 
      user.id === userId 
        ? { ...user, status: user.status === 'active' ? 'suspended' : 'active' }
        : user
    ));
  };
  
  return (
    <div style={{ padding: '20px' }}>
      <header style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px',
        paddingBottom: '20px',
        borderBottom: '1px solid #eee'
      }}>
        <div>
          <h1>⚡ Admin Panel</h1>
          <p>Welcome, {user?.name} (Administrator)</p>
        </div>
        
        <div style={{ display: 'flex', gap: '10px' }}>
          <Link
            to="/dashboard"
            style={{
              padding: '10px 20px',
              backgroundColor: '#6c757d',
              color: 'white',
              textDecoration: 'none',
              borderRadius: '4px'
            }}
          >
            User Dashboard
          </Link>
          <button
            onClick={handleLogout}
            style={{
              padding: '10px 20px',
              backgroundColor: '#dc3545',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            Logout
          </button>
        </div>
      </header>
      
      <div style={{ marginBottom: '30px' }}>
        <h2>📊 System Overview</h2>
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
          gap: '20px',
          marginTop: '20px'
        }}>
          {[
            { label: 'Total Users', value: users.length, color: '#0066cc' },
            { label: 'Active Users', value: users.filter(u => u.status === 'active').length, color: '#28a745' },
            { label: 'Admins', value: users.filter(u => u.role === 'admin').length, color: '#ffc107' },
            { label: 'Suspended', value: users.filter(u => u.status === 'suspended').length, color: '#dc3545' }
          ].map((stat, index) => (
            <div key={index} style={{
              padding: '20px',
              backgroundColor: stat.color,
              color: 'white',
              borderRadius: '8px',
              textAlign: 'center'
            }}>
              <h3 style={{ margin: '0 0 10px 0', fontSize: '2em' }}>{stat.value}</h3>
              <p style={{ margin: 0 }}>{stat.label}</p>
            </div>
          ))}
        </div>
      </div>
      
      <div>
        <h2>👥 User Management</h2>
        <div style={{ marginTop: '20px' }}>
          <table style={{
            width: '100%',
            borderCollapse: 'collapse',
            backgroundColor: 'white',
            borderRadius: '8px',
            overflow: 'hidden',
            boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
          }}>
            <thead>
              <tr style={{ backgroundColor: '#f8f9fa' }}>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Name</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Email</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Role</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Status</th>
                <th style={{ padding: '15px', textAlign: 'left', borderBottom: '1px solid #dee2e6' }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {users.map(user => (
                <tr key={user.id}>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>{user.name}</td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>{user.email}</td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <span style={{
                      padding: '4px 8px',
                      borderRadius: '12px',
                      fontSize: '12px',
                      backgroundColor: user.role === 'admin' ? '#ffc107' : '#0066cc',
                      color: 'white'
                    }}>
                      {user.role}
                    </span>
                  </td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <span style={{
                      padding: '4px 8px',
                      borderRadius: '12px',
                      fontSize: '12px',
                      backgroundColor: user.status === 'active' ? '#28a745' : '#dc3545',
                      color: 'white'
                    }}>
                      {user.status}
                    </span>
                  </td>
                  <td style={{ padding: '15px', borderBottom: '1px solid #dee2e6' }}>
                    <button
                      onClick={() => toggleUserStatus(user.id)}
                      style={{
                        padding: '6px 12px',
                        backgroundColor: user.status === 'active' ? '#dc3545' : '#28a745',
                        color: 'white',
                        border: 'none',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        fontSize: '12px'
                      }}
                    >
                      {user.status === 'active' ? 'Suspend' : 'Activate'}
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

// User Profile Page (Protected)
function UserProfile() {
  const { user } = useAuth();
  const [profile, setProfile] = useState({
    name: user?.name || '',
    email: user?.email || '',
    bio: 'I love building amazing web applications with React!',
    phone: '+1 (555) 123-4567',
    location: 'San Francisco, CA'
  });
  const [isEditing, setIsEditing] = useState(false);
  
  const handleSave = () => {
    // In a real app, you'd save to a backend
    alert('Profile updated successfully!');
    setIsEditing(false);
  };
  
  const handleChange = (e) => {
    setProfile({
      ...profile,
      [e.target.name]: e.target.value
    });
  };
  
  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '30px'
      }}>
        <h1>👤 User Profile</h1>
        <button
          onClick={() => setIsEditing(!isEditing)}
          style={{
            padding: '10px 20px',
            backgroundColor: isEditing ? '#6c757d' : '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          {isEditing ? 'Cancel' : 'Edit Profile'}
        </button>
      </div>
      
      <div style={{
        backgroundColor: 'white',
        padding: '30px',
        borderRadius: '8px',
        border: '1px solid #ddd'
      }}>
        <div style={{ textAlign: 'center', marginBottom: '30px' }}>
          <div style={{
            width: '100px',
            height: '100px',
            borderRadius: '50%',
            backgroundColor: '#0066cc',
            color: 'white',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: '36px',
            margin: '0 auto 15px'
          }}>
            {profile.name.charAt(0).toUpperCase()}
          </div>
          <h2 style={{ margin: '0 0 5px 0' }}>{profile.name}</h2>
          <p style={{ color: '#666', margin: 0 }}>{profile.email}</p>
        </div>
        
        <form>
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Full Name
            </label>
            <input
              type="text"
              name="name"
              value={profile.name}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Email Address
            </label>
            <input
              type="email"
              name="email"
              value={profile.email}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Phone Number
            </label>
            <input
              type="tel"
              name="phone"
              value={profile.phone}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Location
            </label>
            <input
              type="text"
              name="location"
              value={profile.location}
              onChange={handleChange}
              disabled={!isEditing}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa'
              }}
            />
          </div>
          
          <div style={{ marginBottom: '20px' }}>
            <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
              Bio
            </label>
            <textarea
              name="bio"
              value={profile.bio}
              onChange={handleChange}
              disabled={!isEditing}
              rows={4}
              style={{
                width: '100%',
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                backgroundColor: isEditing ? 'white' : '#f8f9fa',
                resize: 'vertical'
              }}
            />
          </div>
          
          {isEditing && (
            <button
              type="button"
              onClick={handleSave}
              style={{
                width: '100%',
                padding: '12px',
                backgroundColor: '#28a745',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                fontSize: '16px'
              }}
            >
              Save Changes
            </button>
          )}
        </form>
      </div>
    </div>
  );
}

Setting Up Protected Routes in Your App#

Now let's put it all together in your main App component:

import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { AuthProvider, useAuth } from './AuthProvider';
import ProtectedRoute from './ProtectedRoute';

// Navigation component that shows different options based on auth state
function Navigation() {
  const { user, isAuthenticated, logout } = useAuth();
  
  const handleLogout = async () => {
    await logout();
  };
  
  return (
    <nav style={{
      backgroundColor: '#333',
      padding: '1rem 0',
      marginBottom: '20px'
    }}>
      <div style={{
        maxWidth: '1200px',
        margin: '0 auto',
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        padding: '0 20px'
      }}>
        <Link 
          to="/" 
          style={{ 
            color: 'white', 
            textDecoration: 'none', 
            fontSize: '24px', 
            fontWeight: 'bold' 
          }}
        >
          SecureApp
        </Link>
        
        <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
          <Link to="/" style={{ color: 'white', textDecoration: 'none' }}>
            Home
          </Link>
          
          {isAuthenticated() ? (
            <>
              <Link to="/dashboard" style={{ color: 'white', textDecoration: 'none' }}>
                Dashboard
              </Link>
              <Link to="/profile" style={{ color: 'white', textDecoration: 'none' }}>
                Profile
              </Link>
              
              {/* Show admin link only for admin users */}
              {user?.roles?.includes('admin') && (
                <Link to="/admin" style={{ color: 'white', textDecoration: 'none' }}>
                  Admin Panel
                </Link>
              )}
              
              <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
                <span style={{ color: 'white' }}>
                  Welcome, {user?.name}
                </span>
                <button
                  onClick={handleLogout}
                  style={{
                    padding: '8px 16px',
                    backgroundColor: '#dc3545',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer'
                  }}
                >
                  Logout
                </button>
              </div>
            </>
          ) : (
            <>
              <Link to="/login" style={{ color: 'white', textDecoration: 'none' }}>
                Login
              </Link>
              <Link 
                to="/register" 
                style={{
                  color: 'white',
                  textDecoration: 'none',
                  padding: '8px 16px',
                  backgroundColor: '#0066cc',
                  borderRadius: '4px'
                }}
              >
                Sign Up
              </Link>
            </>
          )}
        </div>
      </div>
    </nav>
  );
}

// Home page (public)
function HomePage() {
  const { isAuthenticated, user } = useAuth();
  
  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h1>🏠 Welcome to SecureApp</h1>
      <p style={{ fontSize: '18px', marginBottom: '30px' }}>
        A demonstration of protected routes and authentication in React.
      </p>
      
      {isAuthenticated() ? (
        <div>
          <p>Welcome back, {user?.name}!</p>
          <div style={{ display: 'flex', gap: '15px', justifyContent: 'center' }}>
            <Link 
              to="/dashboard"
              style={{
                padding: '12px 24px',
                backgroundColor: '#0066cc',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Go to Dashboard
            </Link>
            <Link 
              to="/profile"
              style={{
                padding: '12px 24px',
                backgroundColor: '#28a745',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              View Profile
            </Link>
          </div>
        </div>
      ) : (
        <div>
          <p>Please log in to access protected features.</p>
          <div style={{ display: 'flex', gap: '15px', justifyContent: 'center' }}>
            <Link 
              to="/login"
              style={{
                padding: '12px 24px',
                backgroundColor: '#0066cc',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Login
            </Link>
            <Link 
              to="/register"
              style={{
                padding: '12px 24px',
                backgroundColor: '#28a745',
                color: 'white',
                textDecoration: 'none',
                borderRadius: '4px'
              }}
            >
              Sign Up
            </Link>
          </div>
        </div>
      )}
      
      <div style={{
        marginTop: '50px',
        padding: '30px',
        backgroundColor: '#f8f9fa',
        borderRadius: '8px',
        maxWidth: '800px',
        margin: '50px auto 0'
      }}>
        <h2>🔒 Security Features</h2>
        <div style={{ 
          display: 'grid', 
          gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
          gap: '20px',
          marginTop: '20px'
        }}>
          <div>
            <h3>✅ Authentication</h3>
            <p>Secure login/logout with token management and session persistence.</p>
          </div>
          <div>
            <h3>🛡️ Route Protection</h3>
            <p>Automatically redirect unauthorized users to login page.</p>
          </div>
          <div>
            <h3>👑 Role-Based Access</h3>
            <p>Different permissions for regular users and administrators.</p>
          </div>
        </div>
      </div>
    </div>
  );
}

// Registration page (public)
function RegisterPage() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    setError('');
    
    if (formData.password !== formData.confirmPassword) {
      setError('Passwords do not match');
      return;
    }
    
    // Simulate registration
    setSuccess(true);
  };
  
  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };
  
  if (success) {
    return (
      <div style={{
        maxWidth: '400px',
        margin: '50px auto',
        padding: '30px',
        textAlign: 'center',
        border: '1px solid #28a745',
        borderRadius: '8px',
        backgroundColor: '#d4edda'
      }}>
        <h2>✅ Registration Successful!</h2>
        <p>Your account has been created. You can now log in.</p>
        <Link 
          to="/login"
          style={{
            padding: '12px 24px',
            backgroundColor: '#28a745',
            color: 'white',
            textDecoration: 'none',
            borderRadius: '4px',
            display: 'inline-block'
          }}
        >
          Go to Login
        </Link>
      </div>
    );
  }
  
  return (
    <div style={{
      maxWidth: '400px',
      margin: '50px auto',
      padding: '30px',
      border: '1px solid #ddd',
      borderRadius: '8px'
    }}>
      <h1 style={{ textAlign: 'center', marginBottom: '30px' }}>
        📝 Create Account
      </h1>
      
      {error && (
        <div style={{
          backgroundColor: '#ffebee',
          color: '#c62828',
          padding: '10px',
          borderRadius: '4px',
          marginBottom: '20px'
        }}>
          ❌ {error}
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Full Name
          </label>
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Email Address
          </label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Password
          </label>
          <input
            type="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label style={{ display: 'block', marginBottom: '5px' }}>
            Confirm Password
          </label>
          <input
            type="password"
            name="confirmPassword"
            value={formData.confirmPassword}
            onChange={handleChange}
            required
            style={{
              width: '100%',
              padding: '10px',
              border: '1px solid #ddd',
              borderRadius: '4px'
            }}
          />
        </div>
        
        <button
          type="submit"
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: '#0066cc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            fontSize: '16px',
            cursor: 'pointer'
          }}
        >
          Create Account
        </button>
      </form>
      
      <div style={{ marginTop: '20px', textAlign: 'center' }}>
        <p>Already have an account?</p>
        <Link to="/login" style={{ color: '#0066cc' }}>
          Sign in here
        </Link>
      </div>
    </div>
  );
}

// Main App Component
function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <div style={{ minHeight: '100vh', backgroundColor: '#f8f9fa' }}>
          <Navigation />
          
          <div style={{ maxWidth: '1200px', margin: '0 auto' }}>
            <Routes>
              {/* Public routes */}
              <Route path="/" element={<HomePage />} />
              <Route path="/login" element={<LoginPage />} />
              <Route path="/register" element={<RegisterPage />} />
              
              {/* Protected routes - require authentication */}
              <Route 
                path="/dashboard" 
                element={
                  <ProtectedRoute>
                    <UserDashboard />
                  </ProtectedRoute>
                } 
              />
              
              <Route 
                path="/profile" 
                element={
                  <ProtectedRoute>
                    <UserProfile />
                  </ProtectedRoute>
                } 
              />
              
              {/* Admin-only route */}
              <Route 
                path="/admin" 
                element={
                  <ProtectedRoute requiredRole="admin">
                    <AdminPanel />
                  </ProtectedRoute>
                } 
              />
              
              {/* 404 page */}
              <Route 
                path="*" 
                element={
                  <div style={{ textAlign: 'center', padding: '50px' }}>
                    <h1>404 - Page Not Found</h1>
                    <Link to="/">Go Home</Link>
                  </div>
                } 
              />
            </Routes>
          </div>
        </div>
      </BrowserRouter>
    </AuthProvider>
  );
}

export default App;

Advanced Protection Patterns#

1. Permission-Based Routes#

// Route that requires specific permissions
<Route 
  path="/edit-post/:postId" 
  element={
    <ProtectedRoute requiredPermission="edit:posts">
      <EditPost />
    </ProtectedRoute>
  } 
/>

// Component with granular permission checks
function PostEditor() {
  const { hasPermission } = useAuth();
  
  return (
    <div>
      <h1>Edit Post</h1>
      
      {hasPermission('publish:posts') && (
        <button>Publish Post</button>
      )}
      
      {hasPermission('delete:posts') && (
        <button>Delete Post</button>
      )}
    </div>
  );
}

2. Conditional Route Protection#

function ConditionalProtectedRoute({ children, condition, fallback }) {
  if (!condition) {
    return fallback || <Navigate to="/unauthorized" />;
  }
  
  return children;
}

// Usage: Protect based on subscription status
<Route 
  path="/premium-features" 
  element={
    <ConditionalProtectedRoute 
      condition={user?.subscription === 'premium'}
      fallback={<UpgradePrompt />}
    >
      <PremiumFeatures />
    </ConditionalProtectedRoute>
  } 
/>

3. Route Guards with Loading States#

function RouteGuard({ children, checkFunction, loadingComponent }) {
  const [isAllowed, setIsAllowed] = useState(null);
  
  useEffect(() => {
    checkFunction().then(setIsAllowed);
  }, [checkFunction]);
  
  if (isAllowed === null) {
    return loadingComponent || <div>Checking permissions...</div>;
  }
  
  if (!isAllowed) {
    return <Navigate to="/unauthorized" />;
  }
  
  return children;
}

Practice Exercise: Multi-Role Dashboard#

Build a dashboard with different views based on user roles:

Requirements:

  1. Guest users: Can only see public content and login prompt
  2. Regular users: Can access personal dashboard and profile
  3. Moderators: Can access user management features
  4. Admins: Can access everything including system settings

Starter structure:

const roleHierarchy = {
  guest: 0,
  user: 1,
  moderator: 2,
  admin: 3
};

function hasMinimumRole(userRole, requiredRole) {
  return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
}

function RoleBasedRoute({ children, minimumRole = 'user' }) {
  // Your implementation here
}

// Usage
<Route 
  path="/moderate" 
  element={
    <RoleBasedRoute minimumRole="moderator">
      <ModerationPanel />
    </RoleBasedRoute>
  } 
/>

Security Best Practices#

1. Never Trust Frontend-Only Protection#

// ❌ Bad - only frontend protection
function AdminPanel() {
  const { user } = useAuth();
  
  if (user?.role !== 'admin') {
    return <div>Access denied</div>;
  }
  
  // Admin functionality - but API calls aren't protected!
  return <div>Delete all users button</div>;
}

// ✅ Good - backend also validates
function AdminPanel() {
  const { user } = useAuth();
  
  if (user?.role !== 'admin') {
    return <div>Access denied</div>;
  }
  
  const deleteUser = async (userId) => {
    // Backend will also check if user is admin
    const response = await fetch('/api/admin/users/' + userId, {
      method: 'DELETE',
      headers: { 
        'Authorization': 'Bearer ' + getToken(),
        'Content-Type': 'application/json'
      }
    });
    
    if (!response.ok) {
      throw new Error('Unauthorized or failed');
    }
  };
  
  return <div>{/* Admin functionality */}</div>;
}

2. Implement Token Refresh#

// Add token refresh logic to your auth context
const refreshToken = async () => {
  const refreshToken = localStorage.getItem('refreshToken');
  
  const response = await fetch('/api/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken })
  });
  
  if (response.ok) {
    const { accessToken } = await response.json();
    localStorage.setItem('authToken', accessToken);
    return accessToken;
  }
  
  // Refresh failed, logout user
  logout();
  return null;
};

3. Handle Concurrent Auth Checks#

// Prevent multiple simultaneous auth checks
let authCheckPromise = null;

const checkAuthStatus = async () => {
  if (authCheckPromise) {
    return authCheckPromise;
  }
  
  authCheckPromise = performAuthCheck();
  const result = await authCheckPromise;
  authCheckPromise = null;
  
  return result;
};

What We've Learned#

Congratulations! You now understand:

✅ What protected routes are and why they're essential for security
✅ How to implement authentication context and state management
✅ Building route guards that check permissions before rendering
✅ Role-based and permission-based access control
✅ Handling login/logout flows with proper redirects
✅ Security best practices and common pitfalls to avoid

Quick Recap Quiz#

Test your protected routes knowledge:

  1. What should you do if a user tries to access a protected route without authentication?
  2. How do you pass the intended destination to the login page?
  3. What's the difference between role-based and permission-based access control?
  4. Why shouldn't you rely only on frontend route protection?

Answers: 1) Redirect to login page with return URL, 2) Use location state or query parameters, 3) Roles are broad categories, permissions are specific actions, 4) Backend must also validate - frontend is for UX only

What's Next?#

In our final lesson, we'll build a Complete React Project that combines everything you've learned! You'll create a full-featured application with components, state management, routing, authentication, and more. This capstone project will demonstrate your mastery of React development.

Protected routes provide the security foundation for building real-world applications!

Previous

What is React?

Next

React Router