A robust authentication flow in React should secure private routes, auto-refresh tokens, and respect user roles, all while ensuring a smooth user experience without forced reloads or unexpected redirects.
🧠 Core Concepts
- Private Route Protection → Redirect unauthenticated users to login.
- Token Expiry Handling → Detect expiry and auto-refresh in the background.
- Silent Refresh → Use refresh tokens to avoid forcing re-login.
- Role-Based Access Control (RBAC) → Show or hide routes based on user roles.
🧾 Step-by-Step Implementation
1. Auth Context to Manage Tokens
import { createContext, useEffect, useState } from "react";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem("user")) || null);
const [accessToken, setAccessToken] = useState(localStorage.getItem("accessToken") || null);
const [refreshToken, setRefreshToken] = useState(localStorage.getItem("refreshToken") || null);
useEffect(() => {
if (accessToken) {
const interval = setInterval(() => {
refreshAccessToken(); // 🔄 silent refresh before expiry
}, 5 * 60 * 1000); // e.g. refresh every 5 mins
return () => clearInterval(interval);
}
}, [accessToken]);
const login = (userData, tokens) => {
setUser(userData);
setAccessToken(tokens.access);
setRefreshToken(tokens.refresh);
localStorage.setItem("user", JSON.stringify(userData));
localStorage.setItem("accessToken", tokens.access);
localStorage.setItem("refreshToken", tokens.refresh);
};
const logout = () => {
setUser(null);
setAccessToken(null);
setRefreshToken(null);
localStorage.clear();
};
const refreshAccessToken = async () => {
try {
const res = await fetch("/api/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken })
});
if (!res.ok) throw new Error("Failed to refresh");
const data = await res.json();
setAccessToken(data.accessToken);
localStorage.setItem("accessToken", data.accessToken);
} catch {
logout(); // if refresh fails, log out
}
};
return (
<AuthContext.Provider value={{ user, accessToken, login, logout }}>
{children}
</AuthContext.Provider>
);
};
2. Create a Private Route Wrapper
import React, { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "./AuthContext";
export default function PrivateRoute({ children, roles = [] }) {
const { user, accessToken } = useContext(AuthContext);
if (!accessToken || !user) {
return <Navigate to="/login" replace />;
}
// 🔐 Role-based access
if (roles.length > 0 && !roles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
}
3. Protect Routes in Router
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PrivateRoute from "./PrivateRoute";
import Dashboard from "./Dashboard";
import AdminPage from "./AdminPage";
import Login from "./Login";
export default function AppRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/admin"
element={
<PrivateRoute roles={["admin"]}>
<AdminPage />
</PrivateRoute>
}
/>
<Route path="*" element={<h2>404 Not Found</h2>} />
</Routes>
</BrowserRouter>
);
}
🧭 How This Works
- 🧑💻 Private Route Protection:
- If no valid token or user is found, redirect to
/login.
- If no valid token or user is found, redirect to
- ⏳ Token Expiry Handling:
- A background timer silently refreshes tokens before expiry using the refresh token.
- If refresh fails, the user is logged out gracefully.
- 🧭 RBAC:
- Roles are checked in the
PrivateRoutecomponent. - Only users with matching roles can access certain routes.
- Roles are checked in the
- 💾 Persistence:
- Tokens and user info are stored in
localStorageto survive page reloads.
- Tokens and user info are stored in
🧪 Pro Tips
- Use Axios interceptors to auto-refresh tokens on 401 errors.
- Store refresh tokens securely in HttpOnly cookies if possible (safer than localStorage).
- For complex apps, consider using React Query + AuthContext for caching & syncing.
- Implement proper server-side token rotation and short-lived access tokens for security.
✅ Final Result:
A secure, production-ready auth flow that:
- Protects private routes,
- Silently refreshes tokens,
- Gracefully handles expiry,
- Supports role-based access control,
- Keeps users logged in without disruptions.