React.js Fundamentals
Authentication in React
Implementing Authentication in React Applications
Authentication is a critical feature in most applications. In this lesson, we'll build a complete authentication system using JWT tokens, including login/register flows, protected routes, token refresh, and OAuth integration.
Authentication Architecture Overview
A typical React authentication system consists of:
- Auth Context: Global authentication state management
- API Service: HTTP requests for login, register, logout
- Token Storage: Secure storage for access and refresh tokens
- Protected Routes: Route guards for authenticated users
- Token Refresh: Automatic token renewal before expiration
- Interceptors: Automatic token attachment to requests
Security First: Never store sensitive data in localStorage without encryption. Use httpOnly cookies for tokens when possible, or implement proper XSS protection.
Auth Context Setup
Create a context to manage authentication state globally:
Auth Context (AuthContext.tsx):
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (name: string, email: string, password: string) => Promise<void>;
logout: () => void;
updateUser: (user: Partial<User>) => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check if user is logged in on mount
checkAuth();
}, []);
const checkAuth = async () => {
try {
const token = localStorage.getItem('accessToken');
if (!token) {
setIsLoading(false);
return;
}
// Verify token and fetch user data
const response = await fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const { user, accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
const register = async (name: string, email: string, password: string) => {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Registration failed');
}
const { user, accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
const logout = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setUser(null);
// Optional: Call logout endpoint to invalidate refresh token
fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
};
const updateUser = (updates: Partial<User>) => {
setUser(prev => prev ? { ...prev, ...updates } : null);
};
const value: AuthContextValue = {
user,
isAuthenticated: !!user,
isLoading,
login,
register,
logout,
updateUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Login and Register Forms
Create user-friendly authentication forms:
Login Form Component:
import { useState, FormEvent } from 'react';
import { useAuth } from './AuthContext';
import { useNavigate, Link } from 'react-router-dom';
function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(email, password);
navigate('/dashboard');
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
<div className="auth-card">
<h1>Welcome Back</h1>
<p>Sign in to your account</p>
{error && (
<div className="alert alert-error">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
placeholder="you@example.com"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
placeholder="••••••••"
/>
</div>
<div className="form-actions">
<Link to="/forgot-password" className="link">
Forgot password?
</Link>
</div>
<button type="submit" disabled={loading} className="btn-primary">
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>
<div className="auth-footer">
Don't have an account?{" "}
<Link to="/register" className="link">Sign up</Link>
</div>
</div>
</div>
);
}
Register Form Component:
import { useState, FormEvent } from 'react';
import { useAuth } from './AuthContext';
import { useNavigate, Link } from 'react-router-dom';
function RegisterPage() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { register } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
// Validation
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
try {
await register(name, email, password);
navigate('/dashboard');
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed');
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
<div className="auth-card">
<h1>Create Account</h1>
<p>Sign up to get started</p>
{error && (
<div className="alert alert-error">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Full Name</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={loading}
placeholder="John Doe"
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
placeholder="you@example.com"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
placeholder="••••••••"
minLength={8}
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
disabled={loading}
placeholder="••••••••"
/>
</div>
<button type="submit" disabled={loading} className="btn-primary">
{loading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<div className="auth-footer">
Already have an account?{" "}
<Link to="/login" className="link">Sign in</Link>
</div>
</div>
</div>
);
}
Protected Routes
Create route guards to protect authenticated pages:
Protected Route Component:
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
}
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return (
<div className="loading-screen">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
if (!isAuthenticated) {
// Redirect to login, but save the location they were trying to visit
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// Usage in routes
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<ProfilePage />
</ProtectedRoute>
}
/>
</Routes>
);
}
Redirect After Login: Use the location state to redirect users back to the page they were trying to access after successful login, providing a better user experience.
Token Refresh Mechanism
Implement automatic token refresh to keep users logged in:
Axios Interceptor with Token Refresh:
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
});
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// Request interceptor - attach token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle 401 and refresh token
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// If error is not 401 or already retried, reject
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
if (isRefreshing) {
// Queue requests while refreshing
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch(err => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
processQueue(new Error('No refresh token'), null);
isRefreshing = false;
// Redirect to login
window.location.href = '/login';
return Promise.reject(error);
}
try {
const response = await axios.post('/api/auth/refresh', {
refreshToken
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
export default api;
OAuth Integration (Google/GitHub)
Implement social login with OAuth providers:
OAuth Login Component:
function OAuthButtons() {
const handleGoogleLogin = () => {
// Redirect to backend OAuth endpoint
window.location.href = '/api/auth/google';
};
const handleGitHubLogin = () => {
window.location.href = '/api/auth/github';
};
return (
<div className="oauth-buttons">
<button onClick={handleGoogleLogin} className="btn-oauth google">
<img src="/google-icon.svg" alt="" />
Continue with Google
</button>
<button onClick={handleGitHubLogin} className="btn-oauth github">
<img src="/github-icon.svg" alt="" />
Continue with GitHub
</button>
<div className="divider">
<span>OR</span>
</div>
</div>
);
}
OAuth Callback Handler:
import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from './AuthContext';
function OAuthCallbackPage() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { updateUser } = useAuth();
useEffect(() => {
const token = searchParams.get('token');
const refreshToken = searchParams.get('refreshToken');
const error = searchParams.get('error');
if (error) {
console.error('OAuth error:', error);
navigate('/login', { state: { error } });
return;
}
if (token && refreshToken) {
localStorage.setItem('accessToken', token);
localStorage.setItem('refreshToken', refreshToken);
// Fetch user data
fetch('/api/auth/me', {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => res.json())
.then(user => {
updateUser(user);
navigate('/dashboard');
})
.catch(() => {
navigate('/login', { state: { error: 'Failed to load user data' } });
});
} else {
navigate('/login', { state: { error: 'Invalid OAuth response' } });
}
}, [searchParams, navigate, updateUser]);
return (
<div className="loading-screen">
<div className="spinner"></div>
<p>Completing sign in...</p>
</div>
);
}
export default OAuthCallbackPage;
Security Considerations: Always validate OAuth tokens on the backend, use HTTPS in production, implement CSRF protection, set short token expiration times, and never expose client secrets in frontend code.
Exercise 1: Complete Auth System
Build a full authentication system with:
- Login, register, and logout functionality
- Protected routes with redirect after login
- Token refresh mechanism
- Password strength indicator on registration
- "Remember me" checkbox (extended token expiration)
Exercise 2: User Profile Management
Create user profile features:
- View and edit profile information
- Change password with current password verification
- Upload and update avatar image
- Email verification flow
- Two-factor authentication setup (bonus)
Exercise 3: Password Reset Flow
Implement forgot/reset password:
- Forgot password page (email input)
- Send reset token via email (mock)
- Reset password page with token validation
- Password strength requirements
- Success/error feedback and redirects