Building authentication in React Router applications: The complete guide for 2026
Authentication in React Router v7 happens in loaders, not useEffect. A complete guide to server-side sessions, protected routes, and enterprise SSO.
React Router v7 changed what authentication means in a React application. With the merger of Remix into React Router, what was once a client-side routing library is now a full-stack framework with server-side rendering, loaders, actions, middleware, and built-in session management. Authentication in this world looks fundamentally different from the client-side token juggling that React developers are used to.
In a React Router v7 app, loaders need to verify sessions before returning data. Actions need to check permissions before writing. Protected routes need server-side guards, not just client-side redirects that a browser's DevTools can bypass. And session management works with encrypted cookies and server-side state, not tokens stored in localStorage.
This guide covers authentication in React Router v7 from the ground up: how the three modes (framework, data, declarative) shape your auth architecture, how to protect routes and manage sessions, what security concerns are specific to React Router, and how to evaluate implementation approaches for your application.
Understanding authentication in React Router v7
React Router v7 operates in three modes, and the mode you choose determines your entire authentication architecture. This is the most important decision you will make before writing any auth code.
The three modes
- Framework mode is the full-stack option. React Router controls your build, your server, and your routing. Loaders and actions run on the server. You have access to request headers, cookies, and server-side sessions. Authentication happens on the server before any data is returned to the client. This is the mode that most closely resembles what Remix offered, and it is the recommended starting point for new applications.
- Data mode adds data loading and mutations to client-side routing. You define loaders and actions on your routes, but they run in the browser unless you set up your own server. Authentication typically involves calling an API from your loaders and managing tokens on the client. You get the structure of loaders and actions without the server-side security guarantees.
- Declarative mode is the classic React Router experience:
<BrowserRouter>,<Route>,<Link>. No loaders, no actions, no server. Authentication is purely client-side, typically using React context anduseEffectto check tokens and redirect. This is the mode most existing React Router tutorials describe, but it provides the weakest security model.
The security difference between these modes is not cosmetic. In framework mode, the user never receives data they are not authorized to see because the loader checks permissions on the server before returning anything. In declarative mode, the data may already be fetched and the redirect is a UI convenience that can be bypassed by anyone who opens the browser's network tab.
Loaders and actions: the authentication primitives
In framework and data modes, loaders and actions replace useEffect and event handlers as the primary places where authentication logic lives.
- Loaders run before a route renders. They receive the
Requestobject (in framework mode) and return data that the component consumes vialoaderData. This is where you verify sessions, check permissions, and fetch user-specific data. - Actions run when a form is submitted or a
fetcher.submit()is called. They handle mutations: login, logout, password changes, and any other state change. In React Router, mutations always go through actions, which means authentication state changes (login, logout) are form submissions, not imperative function calls.
This pattern has an important property: the login form works without JavaScript. Because it uses a standard <form method="post">, the browser submits the form directly to the server. React Router enhances it with client-side navigation when JavaScript is available, but the authentication flow is functional even if JavaScript fails to load. This is progressive enhancement, and it is a core design principle of React Router's framework mode.
Sessions and cookies
React Router (in framework mode) provides built-in session management through cookie-based storage. The API is inherited from Remix and provides createCookieSessionStorage, createSessionStorage, and createFileSessionStorage.
With createCookieSessionStorage, the entire session payload is stored in the cookie itself, encrypted and signed using your secret. This means no external session store is needed, which simplifies deployment. The trade-off is the 4KB cookie size limit, so this works best for small session payloads (user ID, role, a few flags).
For larger session data, use createSessionStorage with a custom backend (Redis, a database, or any key-value store). The cookie then holds only a session ID, and the data lives server-side:
Middleware
React Router v7 introduced middleware for framework mode. Middleware runs before loaders and actions and can set context that downstream functions consume. This is the natural place for authentication logic that applies to multiple routes:
Middleware avoids duplicating session-checking logic across every loader. It is especially useful for layout routes, where a single middleware function protects all child routes.
Authentication implementation approaches
React Router's architecture means your auth approach depends heavily on which mode you are using. The approaches below are organized from the most integrated (framework mode) to the most manual (declarative mode).
Approach 1: Framework mode with cookie sessions
This is the most secure and idiomatic approach for React Router v7 applications. Authentication happens entirely on the server using React Router's built-in session primitives. No tokens are stored in the browser. No client-side auth state to manage.
The login flow works through actions (as shown in the loaders and actions section above). Protected routes check the session in their loader:
Logout is an action, not a navigation:
This approach leverages React Router's strengths: server-side data loading, progressive enhancement, and encrypted cookie sessions. Data is never sent to an unauthorized user because the redirect happens in the loader, before any rendering occurs.
For applications with many protected routes, use a layout route with middleware or a shared loader to avoid repeating session checks in every route module.
Approach 2: Framework mode with JWT validation
If your React Router app consumes an external API that issues JWTs, you can validate tokens server-side in your loaders while still using React Router's session management for the web session.
The key insight: even when working with JWTs, the tokens stay on the server inside the encrypted session cookie. The browser never sees the raw JWT. This gives you the security benefits of server-side session management while integrating with token-based APIs.
Approach 3: Data mode with client-side authentication
In data mode, loaders and actions run in the browser. Authentication relies on tokens stored in memory or cookies and validated against your API.
Data mode gives you the organizational benefits of loaders and actions (separation of data fetching from rendering, automatic loading states) without requiring a server. However, authentication state lives in the browser, making it visible to client-side code and dependent on your API for validation.
This approach is reasonable when you are deploying a static SPA with a separate backend API and don't need server-side rendering. For new projects, framework mode is a stronger default.
Approach 4: Declarative mode with context and protected routes
This is the traditional React pattern: an AuthContext that wraps your app, a useAuth hook, and a ProtectedRoute component that checks authentication status.
This pattern works and is widely documented, but it has real limitations. The useEffect call on mount means there is always a loading flash while the auth state is determined. The redirect is client-side only, so an unauthenticated user briefly receives the page markup before being bounced. And because there is no server layer, token management (storage, refresh, expiration) lives entirely in client code.
If you are maintaining an existing declarative-mode app, this pattern is functional. For new projects, consider migrating to data mode or framework mode for stronger auth guarantees.
Approach 5: Managed authentication provider
The approaches above all require you to build the authentication infrastructure yourself: session storage, credential verification, token refresh, password hashing, email verification, MFA, and the ongoing maintenance to keep it secure. Even in framework mode where React Router provides strong session primitives, the authentication logic, user management, and enterprise features are your responsibility.
A managed provider handles all of this externally and integrates into your React Router application through its data loading model. The right provider plugs directly into loaders and actions with server-side session management, rather than requiring client-side context providers and token juggling.
This distinction matters in React Router. Providers without a dedicated React Router SDK force you to wire up authentication yourself: manually reading cookies in every loader, validating tokens against a JWKS endpoint, and handling refresh logic across server and client boundaries. That is doable, but it compounds across every protected route in your application.
WorkOS is a strong fit here. It provides @workos-inc/authkit-react-router, a first-party SDK built specifically for React Router v7 framework mode. The SDK provides authkitLoader for protecting routes in loaders, cookie-based encrypted sessions, and server-side access token handling that plugs directly into React Router's data loading model.
The fastest way to get started is the WorkOS CLI, which detects your React Router setup, installs the SDK, configures your dashboard, and writes the integration code:
You can also set up the integration manually. Install the SDK and configure your environment:
Protecting a route is a single function call:
authkitLoader checks the encrypted session cookie, validates the session, refreshes tokens if needed, and either returns the authenticated user or redirects to AuthKit's hosted login page. One function replaces all the manual session management code from Approach 1.
For routes that should show different content to authenticated and unauthenticated users (rather than redirecting), omit ensureSignedIn:
Logout works through actions, following React Router's conventions:
For advanced use cases, withAuth provides direct access to the authentication data without the automatic redirect behavior of authkitLoader. And getWorkOS() gives you the full WorkOS SDK client for accessing organizations, directory sync, audit logs, and other platform features.
Beyond basic authentication, WorkOS provides enterprise SSO (SAML and OIDC), SCIM-based directory sync, organization management with multi-tenancy, audit logs, bot protection, and compliance features. These capabilities build on each other as your requirements grow, and the React Router SDK integrates them through the same loader-based pattern.
This approach makes the most sense for B2B software where enterprise customers will eventually require SSO, directory sync, or compliance certifications. Rather than building those features over months and maintaining them indefinitely, you delegate them to a platform designed for that purpose and keep your team focused on your product.
Security considerations for React Router authentication
Now that you have seen the implementation patterns, here is what can go wrong and what to watch for.
Client-side redirects are not security
The most common mistake in React Router authentication is treating client-side redirects as access control. A <Navigate> component or a redirect() in a client-side loader prevents a user from seeing a page, but it does not prevent them from accessing the data.
If your API returns data to any request with valid headers, an unauthenticated user can open the browser's network tab, copy the fetch request, and replay it. The <Navigate> component only hides the UI; it does not protect the data.
In framework mode, loader-based redirects are genuine access control because the redirect happens on the server before any data is returned:
If you are in declarative or data mode, your API must enforce authentication and authorization independently of the frontend. The React Router redirect is a UX convenience, not a security boundary.
CSRF protection through actions
React Router's action model provides natural CSRF protection in framework mode. Because mutations happen through <form method="post"> and actions, and because React Router automatically includes cookies with these requests, the sameSite: "lax" cookie attribute prevents cross-site form submissions.
This works because sameSite: "lax" allows cookies on same-site navigations (GET requests) but blocks them on cross-site form submissions (POST, PUT, DELETE). Since loaders handle GET and actions handle POST, the architecture aligns with the cookie's security model.
However, if you are building an API that accepts JSON bodies (not form data), you lose the browser's built-in CSRF protection. In that case, implement CSRF tokens explicitly:
Session cookie configuration
Every cookie used for authentication should be configured defensively:
httpOnly: true is non-negotiable. Without it, any XSS vulnerability in your application can steal session cookies via document.cookie. secure: true ensures cookies are only transmitted over HTTPS. sameSite: "lax" prevents cross-site request forgery while still allowing normal link navigation. The secrets array supports secret rotation: add a new secret to the front and keep the old one at the end. New sessions use the first secret; existing sessions signed with any secret in the array will still validate.
Do not store tokens in localStorage
This advice appears in every authentication guide because it remains the most common mistake in React applications. localStorage is accessible to any JavaScript running on the page. A single XSS vulnerability (a compromised dependency, an unescaped user input, a malicious browser extension) can read every token in localStorage and exfiltrate it.
In framework mode, this is a non-issue because tokens live in encrypted httpOnly cookies that JavaScript cannot read. In declarative or data mode, store tokens in httpOnly cookies via your API, not in localStorage or sessionStorage.
Flash messages and race conditions
React Router's session.flash() writes a value to the session that is automatically cleared after the next read. This is useful for showing error messages after a failed login:
The race condition risk: because of nested routes, multiple loaders can run in parallel to construct a single page. If two loaders read the same flash value, the first one gets it and the second gets nothing (or the flash is consumed before either reads it, depending on timing).
Use flash messages only in leaf loaders that are the sole consumer of the flash value. If multiple loaders need error state, use distinct flash keys for each.
XSS and React's built-in protections
React escapes content in JSX expressions by default, preventing most XSS attacks:
Never use dangerouslySetInnerHTML with user-provided content unless you sanitize it first with a library like DOMPurify. In an authentication context, XSS is especially dangerous because it can steal session cookies (if httpOnly is not set), exfiltrate tokens from memory or localStorage, or perform actions as the authenticated user.
Dependency supply chain risks
React applications typically have hundreds of npm dependencies. Each is a potential vector for malicious code that can steal tokens, exfiltrate cookies (if httpOnly is not set), or inject scripts.
Run npm audit in CI. Use package-lock.json and commit it to version control. Audit new dependencies before adding them. For authentication-specific packages, prefer well-maintained libraries with security audit histories.
Production best practices
Security checklist
- Use framework mode for authentication whenever possible. Server-side session validation is fundamentally more secure than client-side token checking.
- Set
httpOnly,secure, andsameSiteon all session cookies. Never omithttpOnly. - Do not store tokens in
localStorageorsessionStorage. Use encrypted httpOnly cookies. - Perform all mutations (login, logout, password changes) through actions, not GET requests in loaders. Loaders should be safe to call without side effects.
- Validate and sanitize all form data in actions before processing. Never trust client input.
- Never use
dangerouslySetInnerHTMLwith user-provided content. - Keep React, React Router, and all npm dependencies updated. Run
npm auditin CI. - Use
package-lock.jsonand commit it to version control. - Rotate session secrets without downtime by using the
secretsarray in your cookie configuration. - Implement rate limiting on login and registration actions (via middleware or a reverse proxy).
- Log authentication events (logins, failures, logouts) and monitor for anomalies.
Deployment checklist
- Generate a strong session secret (at least 32 characters) using
openssl rand -hex 32or a secrets manager. - Set
secure: trueon all cookies in production. Test that your deployment serves over HTTPS. - Configure your reverse proxy or CDN to forward cookies correctly. Verify that
Set-Cookieheaders are not stripped. - If using
createCookieSessionStorage, keep session payloads small (under 4KB) to avoid exceeding cookie size limits. - If using
createSessionStoragewith a database or Redis backend, configure connection pooling and session expiration. - Test authentication flows end to end: login, logout, session expiration, token refresh, and protected route access.
- Verify that unauthenticated requests to protected loaders receive redirects, not partial data.
- Set up monitoring for authentication endpoint errors and latency spikes.
- Configure proper cache headers to prevent authenticated responses from being cached by CDNs or browsers.
Conclusion
React Router v7 changed the game for authentication in React applications. With server-side loaders, actions, encrypted cookie sessions, and middleware, it provides the primitives to build authentication that is secure by default. The key is choosing the right mode and using it correctly.
If you are building authentication yourself, use framework mode and lean on React Router's server-side session management. Keep tokens out of the browser. Perform mutations through actions. Treat client-side redirects as UX, not security.
If you are considering a managed provider, prioritize one with a dedicated React Router SDK that integrates at the loader level, not one that relies on client-side context providers designed for SPAs.
Authentication is critical infrastructure. Choose the approach that matches where your application is headed, not just where it is today.
Sign up for WorkOS and secure your React Router application.