To create an Auth0 protected route in React, wrap your app in Auth0Provider, then put private routes inside a layout route that reads isLoading and isAuthenticated from useAuth0(): show a loading state while Auth0 checks the session, call loginWithRedirect() with the current path in appState if the user isn’t logged in, and render <Outlet /> if they are. The SDK’s withAuthenticationRequired helper does the same for a single component.
If you are building a React application with Auth0 for user management, you will face this problem sooner or later. I faced it too, when the protected-route examples I found were written for React Router v5 and no longer worked. This guide is updated for React Router 8 and @auth0/auth0-react 2.28, and fixes a bug in my original 2023 version.
How Auth0 Login Works in a React App
- A user opens a private page, such as
/dashboard. - Your protected route sees they are not logged in and calls
loginWithRedirect(), saving/dashboardinappState. - The browser goes to Auth0’s hosted Universal Login page, where the user signs in with a password or a social login.
- Auth0 redirects back to your app. The SDK exchanges the authorization code for tokens (using the Authorization Code flow with PKCE) and calls your
onRedirectCallbackwith the savedappState. - You navigate to
appState.returnTo, and the user lands on/dashboard, now logged in.
Step 1: Create an Auth0 Application
- In the Auth0 dashboard, go to Applications → Create Application and choose Single Page Web Applications.
- In its settings, add your app’s URL (for example
http://localhost:5173during development) to Allowed Callback URLs, Allowed Logout URLs and Allowed Web Origins. - Copy the Domain and Client ID. If you will call your own API, also create an API under Applications → APIs and note its Identifier (the audience).
Put them in a .env file. These values are public by design for a single-page app; there is no client secret in browser code:
VITE_AUTH0_DOMAIN=your-tenant.us.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id
VITE_AUTH0_AUDIENCE=https://api.example.comStep 2: Install the Packages
npm install @auth0/auth0-react react-routerReact Router 8 removed the react-router-dom package, so import everything from react-router. If you’re new to it, start with my React Router tutorial for beginners.
Step 3: Add Auth0Provider Inside the Router
Put the provider inside <BrowserRouter>, so the redirect callback can use React Router’s navigate instead of a full page reload:
import { Auth0Provider } from '@auth0/auth0-react';
import { useNavigate } from 'react-router';
// Must be rendered inside <BrowserRouter> so it can use useNavigate()
export default function Auth0ProviderWithNavigate({ children }) {
const navigate = useNavigate();
// After login, Auth0 redirects back to your app; send the user where they were going
const onRedirectCallback = (appState) => {
navigate(appState?.returnTo ?? '/', { replace: true });
};
return (
<Auth0Provider
domain={import.meta.env.VITE_AUTH0_DOMAIN}
clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}
authorizationParams={{
redirect_uri: window.location.origin,
audience: import.meta.env.VITE_AUTH0_AUDIENCE, // only needed to call your own API
}}
onRedirectCallback={onRedirectCallback}
>
{children}
</Auth0Provider>
);
}import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import Auth0ProviderWithNavigate from './Auth0ProviderWithNavigate.jsx';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<Auth0ProviderWithNavigate>
<App />
</Auth0ProviderWithNavigate>
</BrowserRouter>
</StrictMode>,
);My original version created a separate history object with the history package for this. That isn’t needed (or supported) in React Router 6 and later; useNavigate() replaces it.
Step 4: Create the Protected Route
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router';
import { useAuth0 } from '@auth0/auth0-react';
export default function ProtectedRoute() {
const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0();
const location = useLocation();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
// Remember the page so onRedirectCallback can bring the user back to it
loginWithRedirect({ appState: { returnTo: location.pathname + location.search } });
}
}, [isLoading, isAuthenticated, loginWithRedirect, location]);
if (isLoading) return <p>Checking your session...</p>; // don't redirect before Auth0 has checked
if (!isAuthenticated) return <p>Redirecting to login...</p>;
return <Outlet />;
}Three details make this reliable:
- Wait for
isLoading. On every page load, the SDK first checks whether the user already has a session. Until that check finishes,isAuthenticatedisfalseeven for logged-in users, so redirecting immediately would send everyone to the login page. - Call
loginWithRedirectin an effect, not during render. It starts a navigation and returns a promise. The 2023 version of this post wrote<Navigate to={loginWithRedirect()} />, which passes a promise as a URL. It happened to redirect because the function ran during render, but it is not valid usage. - Save the current path in
appState, including the query string, soonRedirectCallbackcan return the user exactly where they were.
Step 5: Add It to Your Routes
Because ProtectedRoute renders an <Outlet />, it works as a layout route: every route nested inside it is protected.
import { Routes, Route, Link } from 'react-router';
import { useAuth0, withAuthenticationRequired } from '@auth0/auth0-react';
import ProtectedRoute from './ProtectedRoute.jsx';
import Profile from './Profile.jsx';
const Home = () => <h1>Home (public)</h1>;
const Dashboard = () => <h1>Dashboard (private)</h1>;
// Alternative: protect a single component with the SDK's built-in HOC
const Settings = withAuthenticationRequired(() => <h1>Settings (private)</h1>, {
onRedirecting: () => <p>Redirecting to login...</p>,
});
function Nav() {
const { isAuthenticated, loginWithRedirect, logout } = useAuth0();
return (
<nav>
<Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link> | <Link to="/profile">Profile</Link> |{' '}
{isAuthenticated ? (
<button onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}>Log out</button>
) : (
<button onClick={() => loginWithRedirect()}>Log in</button>
)}
</nav>
);
}
export default function App() {
return (
<>
<Nav />
<Routes>
<Route path="/" element={<Home />} />
<Route element={<ProtectedRoute />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
</Route>
<Route path="settings" element={<Settings />} />
</Routes>
</>
);
}Alternative: withAuthenticationRequired
The SDK’s withAuthenticationRequired higher-order component (used for Settings above) protects a single component with the same logic, including waiting for isLoading and returning to the original page. Use it for one-off pages, and the layout route for whole sections of your app. If you’re curious how this pattern works, see my guide to higher-order components in React.
Step 6: Call Your API with an Access Token
A protected route only hides UI. To protect data, your API must check an access token on every request. Get one with getAccessTokenSilently(), which returns a cached token and refreshes it when needed:
import { useEffect, useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
export default function Profile() {
const { user, getAccessTokenSilently } = useAuth0();
const [orders, setOrders] = useState(null);
useEffect(() => {
(async () => {
const token = await getAccessTokenSilently(); // cached, refreshed when needed
const res = await fetch('/api/orders', { headers: { Authorization: `Bearer ${token}` } });
setOrders(res.ok ? await res.json() : []);
})();
}, [getAccessTokenSilently]);
return (
<>
<h1>{user?.name}</h1>
<p id="email">{user?.email}</p>
<p id="orders">{orders ? `${orders.length} orders` : 'Loading orders...'}</p>
</>
);
}On the server, validate the token’s signature, issuer and audience (Auth0 provides middleware for Express and other frameworks). The user object in the browser holds profile information such as name, email and picture; it never contains the user’s password.
How I Tested It
I don’t have a live Auth0 tenant for this update, so I tested the routing logic with the real @auth0/auth0-react 2.28.2 and React Router 8.5 components, supplying the SDK’s own Auth0Context with a test value in a production build, and drove it with Puppeteer in headless Brave:
| Visited | Auth state | Result |
|---|---|---|
/dashboard?tab=2 | Still checking session | “Checking your session…” and no redirect |
/dashboard?tab=2 | Logged out | loginWithRedirect called once with appState.returnTo = /dashboard?tab=2 |
/dashboard | Logged in | Dashboard rendered |
/profile | Logged in | Name and email shown; API called with Authorization: Bearer <token> |
/settings (HOC) | Logged out / in | Redirected to login / rendered |
| Log out button | Logged in | logout called with returnTo set to the app’s origin |
The actual login page and token exchange are handled by Auth0, as shown in the video above. In development, React’s Strict Mode runs effects twice, so you may see the redirect start twice; that does not happen in production builds.
Common Problems
- Infinite redirect loop: usually the callback URL isn’t in Allowed Callback URLs, or you redirect before
isLoadingis false. - “Callback URL mismatch” error: the
redirect_urimust match an Allowed Callback URL exactly, including the port and any trailing slash. - Logged out after refresh: by default tokens are kept in memory. Enable refresh tokens (
useRefreshTokens) with rotation in Auth0, and consider a custom domain so browsers that block third-party cookies keep the session. - Lands on the home page after login: pass
appState.returnToand handle it inonRedirectCallback. - API returns 401: set
audienceinauthorizationParamsso the access token is issued for your API, and check the audience on the server.
Conclusion
A reliable Auth0 protected route in React Router 8 comes down to three things: put Auth0Provider inside the router and navigate in onRedirectCallback, wait for isLoading before deciding, and call loginWithRedirect with the current path in appState from an effect. Use a layout route with <Outlet /> for sections of your app, withAuthenticationRequired for single pages, and always enforce access on your API with access tokens. Auth0’s React quickstart covers the dashboard setup in more detail.
FAQs
How do I protect a route with Auth0 in React?
Create a component that reads isLoading and isAuthenticated from useAuth0(). Show a loader while loading, call loginWithRedirect({ appState: { returnTo: currentPath } }) in an effect when the user is not authenticated, and render the page (or an Outlet) when they are. Or wrap the page with withAuthenticationRequired.
Why does my Auth0 protected route redirect to login even when I am logged in?
You are probably checking isAuthenticated before the SDK has finished restoring the session. Wait until isLoading is false before deciding to redirect.
How do I return users to the page they wanted after Auth0 login?
Pass the current path in appState when calling loginWithRedirect, then navigate to appState.returnTo in the onRedirectCallback prop of Auth0Provider.
Should I use withAuthenticationRequired or a custom protected route?
Both work. withAuthenticationRequired is simplest for individual components. A custom layout route with an Outlet is convenient for protecting a whole group of routes in React Router.
Is a protected route enough to secure my app?
No. It only controls what the browser shows. Protect data by validating Auth0 access tokens on your API for every request.
Does @auth0/auth0-react work with React Router 8 and React 19?
Yes. I tested @auth0/auth0-react 2.28.2 with React 19 and React Router 8.5; the SDK supports React 16.11 through 19.








