Protected Routes
Learn how to protect routes with authentication and authorization, creating secure navigation patterns in your React applications.
Learning Objectives
- Understand what protected routes are and why they're important
- Implement authentication state management
- Build route guards that redirect unauthorized users
- Create role-based authorization systems
- Handle login/logout flows with proper redirects
- Build secure navigation patterns and user experiences
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:
- Guest users: Can only see public content and login prompt
- Regular users: Can access personal dashboard and profile
- Moderators: Can access user management features
- 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:
- What should you do if a user tries to access a protected route without authentication?
- How do you pass the intended destination to the login page?
- What's the difference between role-based and permission-based access control?
- 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!