Learn how to protect routes with authentication and authorization, creating secure navigation patterns in your React applications.
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.
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:
Protected routes are React components that:
Think of them as smart bouncers for your app!
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']
};
}
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;
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>
);
}
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;
// 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>
);
}
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>
}
/>
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;
}
Build a dashboard with different views based on user roles:
Requirements:
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>
}
/>
// โ 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>;
}
// 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;
};
// 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;
};
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
Test your protected routes knowledge:
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
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!