Authentication & Auth Patterns
Protected routes, auth context, JWT/session handling, and login flows in React
Authentication in React
Authentication is a critical part of most web applications. React handles the frontend side β login/signup forms, storing tokens, protecting routes, and managing user sessions. The actual authentication logic (verifying credentials, issuing tokens) happens on the backend.
π Auth Flow Overview
- User enters credentials in a login form
- Frontend sends credentials to the backend API
- Backend verifies and returns a token (JWT) or sets a session cookie
- Frontend stores the token and includes it in subsequent API requests
- Protected routes check for valid authentication before rendering
Building an Auth Context
Create a context that provides auth state throughout your app:
// context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Check for existing session on mount
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
try {
const token = localStorage.getItem('token');
if (!token) {
setLoading(false);
return;
}
// Verify token with backend
const res = await fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const userData = await res.json();
setUser(userData);
} else {
localStorage.removeItem('token');
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
}
async function login(email, password) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.message || 'Login failed');
}
const { user, token } = await res.json();
localStorage.setItem('token', token);
setUser(user);
return user;
}
async function signup(email, password, name) {
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.message || 'Signup failed');
}
const { user, token } = await res.json();
localStorage.setItem('token', token);
setUser(user);
return user;
}
function logout() {
localStorage.removeItem('token');
setUser(null);
}
return (
<AuthContext.Provider value={{ user, loading, login, signup, logout }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook for easy access
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Login and Signup Forms
// components/LoginForm.jsx
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
setError('');
setIsLoading(true);
try {
await login(email, password);
navigate('/dashboard');
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto space-y-4">
<h2 className="text-2xl font-bold">Log In</h2>
{error && (
<div className="p-3 bg-red-50 text-red-700 rounded">{error}</div>
)}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
className="w-full p-2 border rounded"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{isLoading ? 'Logging in...' : 'Log In'}
</button>
</form>
);
}
Protected Routes
Create a wrapper component that redirects unauthenticated users:
// components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function ProtectedRoute({ children }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full" />
</div>
);
}
if (!user) {
// Redirect to login, preserving the intended destination
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Role-based protection
export function AdminRoute({ children }) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" replace />;
if (user.role !== 'admin') return <Navigate to="/unauthorized" replace />;
return children;
}
// Usage in router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route path="/signup" element={<SignupForm />} />
{/* Protected routes */}
<Route path="/dashboard" element={
<ProtectedRoute><Dashboard /></ProtectedRoute>
} />
<Route path="/profile" element={
<ProtectedRoute><Profile /></ProtectedRoute>
} />
<Route path="/admin" element={
<AdminRoute><AdminPanel /></AdminRoute>
} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
Authenticated API Requests
Create a wrapper around fetch that automatically includes the auth token:
// lib/api.js
export async function authFetch(url, options = {}) {
const token = localStorage.getItem('token');
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
// Handle token expiration
if (res.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
throw new Error('Session expired');
}
return res;
}
// Custom hook for authenticated data fetching
function useAuthQuery(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
authFetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
function Dashboard() {
const { data: stats, loading } = useAuthQuery('/api/dashboard/stats');
if (loading) return <Skeleton />;
return <StatsGrid data={stats} />;
}
Showing Auth-Dependent UI
// components/Navbar.jsx
import { useAuth } from '../context/AuthContext';
import { Link } from 'react-router-dom';
function Navbar() {
const { user, logout } = useAuth();
return (
<nav className="flex items-center justify-between p-4 border-b">
<Link to="/" className="font-bold text-xl">MyApp</Link>
<div className="flex items-center gap-4">
{user ? (
<>
<span>Hello, {user.name}</span>
<Link to="/dashboard">Dashboard</Link>
{user.role === 'admin' && <Link to="/admin">Admin</Link>}
<button
onClick={logout}
className="px-3 py-1 bg-red-500 text-white rounded"
>
Log Out
</button>
</>
) : (
<>
<Link to="/login">Log In</Link>
<Link to="/signup" className="px-3 py-1 bg-blue-500 text-white rounded">
Sign Up
</Link>
</>
)}
</div>
</nav>
);
}
π Security Best Practices
- β’ Never store sensitive data in localStorage in production β use httpOnly cookies for tokens when possible
- β’ Always validate on the server β frontend auth is for UX, not security
- β’ Use HTTPS for all API requests
- β’ Implement token refresh β don't use long-lived access tokens
- β’ Clear auth state on logout (tokens, cached data, cookies)
- β’ Protect against XSS β sanitize inputs, use Content Security Policy
- β’ Handle token expiration gracefully β redirect to login, don't crash
β Production Auth Libraries
For production apps, consider using established auth solutions:
- β’ NextAuth.js / Auth.js β Full-featured auth for Next.js (OAuth, credentials, sessions)
- β’ Firebase Auth β Google's auth service with social providers
- β’ Supabase Auth β Open-source auth with row-level security
- β’ Clerk β Drop-in auth components with user management
- β’ Auth0 β Enterprise auth with extensive customization