🔐 Auth Synchronization: JWTs and the Browser
In Phase 3, we built an Express backend that generated a JWT Passport when a user logged in, and verified that Passport via an Auth Middleware.
Now, we must configure the React Frontend to catch that Passport, store it securely, and seamlessly attach it to every single subsequent API request. If you mess this up, users will have to log in every time they click a button.
1️⃣ The Login Synergy Flow
When the user clicks the "Login" button, React fires off a POST request. Express validates the password and responds with the JWT. React must immediately grab it and save it.
2️⃣ Axios Interceptors (The Automatic Stamp)
If we store the JWT in LocalStorage, we must manually attach it to the Authorization: Bearer header on every single axios.get() call. This is incredibly repetitive.
An Axios Interceptor is a global setting that acts like a border officer. For every single HTTP request leaving your React app, the Interceptor stops it, grabs the JWT from LocalStorage, stamps it onto the Header, and sends it on its way.
// src/services/apiService.js
import axios from 'axios';
const API = axios.create({ baseURL: 'http://localhost:5000/api' });
// 🛂 THE REQUEST INTERCEPTOR
API.interceptors.request.use((config) => {
// 1. Before the request leaves the browser, grab the passport
const token = localStorage.getItem('token');
// 2. If we have a passport, violently stamp it onto the HTTP Headers
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 3. Let the request continue to the server
return config;
}, (error) => {
return Promise.reject(error);
});
export default API;
Now, inside your React components, you simply type API.get('/private-data'). You never have to think about the token again. The Interceptor handles it invisibly!
3️⃣ Syncing Auth State across the App (Context)
Once a user logs in, the <Navbar> needs to change from "Login" to a Profile Picture. But the login sequence happened deep inside the <LoginForm> component. How does the Navbar know?
We must use React's useContext to blanket the entire application in a master AuthContext state.
// AuthContext.js
import { createContext, useState, useEffect } from 'react';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Initial Load Check
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Wait, we have a passport, but who is it for?
// We must ask the server: "Here is my token, who am I?"
fetchSystemUser(token);
} else {
setLoading(false);
}
}, []);
const fetchSystemUser = async () => {
try {
// The Interceptor will automatically attach the token here!
const response = await API.get('/auth/me');
setUser(response.data); // Set the logged-in user!
} catch (error) {
// Token was expired or fake. Wipe it.
logout();
} finally {
setLoading(false);
}
};
const login = (token, userData) => {
localStorage.setItem('token', token);
setUser(userData);
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
};
4️⃣ Protecting React Router (Frontend Blacklists)
Express blocks unauthorized backend requests with its requireAuth middleware. However, a user could still literally type http://localhost:3000/admin-dashboard into their URL bar and stare at massive, broken React components.
We must build Protected Routes in React Router to intercept unauthorized page navigations and aggressively boot them back to the login screen.
// ProtectedRoute.js
import { Navigate } from 'react-router-dom';
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
// A wrapper component!
function ProtectedRoute({ children }) {
// Tap into the master state portal
const { user, loading } = useContext(AuthContext);
// Stop execution if the initial check hasn't finished
if (loading) return <div>Checking secure credentials...</div>;
// The Kill Switch: If there is no user, teleport them to /login and replace their
// browser history so they can't click 'Back'.
if (!user) {
return <Navigate to="/login" replace />;
}
// Access Granted! Render the protected component they asked for.
return children;
}
Implementing it in App.js:
<Routes>
{/* Public Routes */}
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
{/* Locked Routes */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
</Routes>
💡 Summary Checklist for Auth Synergy
- Does Express return the JWT cleanly on
POST /login? - Does React save the JWT somewhere persistent (LocalStorage or Cookies)?
- Does the Axios Interceptor automatically extract that token and stamp it on every outgoing
GET/POST/PUT/DELETE? - Does the
AuthContextstore the current User's profile data globally so the Navbar updates instantly? - Does
<ProtectedRoute>mathematically prevent unauthenticated guests from rendering the<Dashboard>component?