> ## Documentation Index
> Fetch the complete documentation index at: https://auth0-feat-react-router-quickstart.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Login to Your React Router Application

> This guide demonstrates how to integrate Auth0 with a React Router application (framework mode, v7 or later) using the Auth0 React Router SDK.

export const HowToSchema = () => <script type="application/ld+json">
    {'{"@context":"https://schema.org","@type":"HowTo"}'}
  </script>;

export const CreateInteractiveApp = ({placeholderText = 'Auth0', appType = 'regular_web', allowedCallbackUrls = ['localhost:3000'], allowedLogoutUrls = ['localhost:3000'], allowedOriginUrls = ['localhost:3000']}) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [storeReady, setStoreReady] = useState(false);
  const [displayForm, setDisplayForm] = useState(true);
  useEffect(() => {
    const init = () => setStoreReady(true);
    if (window.rootStore) {
      window.rootStore.clientStore.setSelectedClient(null);
      window.rootStore.clientStore.setSelectedClientSecret(undefined);
      init();
    } else {
      window.addEventListener('adu:storeReady', init);
    }
    return () => {
      window.removeEventListener('adu:storeReady', init);
    };
  }, []);
  useEffect(() => {
    if (!storeReady) return;
    const disposer = autorun(() => {
      const rootStore = window.rootStore;
      setIsAuthenticated(rootStore.sessionStore.isAuthenticated);
    });
    return () => {
      disposer();
    };
  }, [storeReady]);
  if (!storeReady || typeof window === 'undefined' || !displayForm) {
    return <></>;
  }
  const login = () => {
    const baseUrl = window.rootStore.config.apiBaseUrl;
    const returnTo = encodeURIComponent(window.location.href);
    window.location.href = `${baseUrl}/auth/user/login?returnTo=${returnTo}`;
  };
  const Card = ({className = '', children}) => {
    return <div className={`
          flex border rounded-2xl
          border-gray-950/10 dark:border-white/10
          py-3.5 px-4 gap-2
          text-sm text-gray-900 dark:text-gray-200
          ${className}
        `}>
        {children}
      </div>;
  };
  const Button = ({children, ...props}) => {
    return <button className="bg-[--button-primary] text-[--foreground-inverse] px-[1.125rem] py-1.5 rounded-lg font-medium" {...props}>
        {children}
      </button>;
  };
  const CreateApplicationForm = () => {
    const [name, setName] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState('');
    const handleSubmit = async () => {
      if (!name.trim()) {
        setError('Application name is required');
        return;
      }
      setIsLoading(true);
      setError(null);
      try {
        await window.rootStore.clientStore.createClient({
          name: name.trim(),
          app_type: appType,
          callbacks: allowedCallbackUrls,
          allowed_logout_urls: allowedLogoutUrls,
          web_origins: allowedOriginUrls,
          client_metadata: {
            created_by: 'quickstart-docs-app-creation-component'
          }
        });
        setDisplayForm(false);
      } catch (err) {
        console.error('Error creating client:', err);
        const errorMessage = err instanceof Error ? err.message : 'Failed to create application';
        setError(errorMessage);
      } finally {
        setIsLoading(false);
      }
    };
    return <Card className="flex-col items-start p-4 gap-3.75">
        <span className="font-medium text-gray-900 dark:text-gray-200">
          Create Auth0 App
        </span>
        <div className="w-full flex gap-2">
          <input id="app-name" name={name} className="
              w-full max-w-[448px] h-11 py-2 px-4 
              border rounded-lg border-gray-950/10 dark:border-white/10 
              text-gray-900 dark:text-gray-200
              focus:outline-none dark:focus:outline-none
            " placeholder={`My ${placeholderText} App`} value={name} onChange={e => setName(e.target.value)} />
          <Button onClick={handleSubmit}>
            {isLoading ? 'Creating...' : 'Create'}
          </Button>
        </div>
        {error && <p className="text-red-500">{error}</p>}
      </Card>;
  };
  const SignInForm = () => {
    return <Card className="items-center">
        <Button onClick={login}>Log in</Button> <span>to create the app</span>
      </Card>;
  };
  return isAuthenticated ? <CreateApplicationForm /> : <SignInForm />;
};

export const AuthCodeBlock = ({filename, icon, language, highlight, children}) => {
  const [displayText, setDisplayText] = useState(children);
  const [copyText, setCopyText] = useState(children);
  const wrapperRef = React.useRef(null);
  useEffect(() => {
    let unsubscribe = null;
    function init() {
      if (!window.autorun || !window.rootStore) {
        return;
      }
      unsubscribe = window.autorun(() => {
        let processedChildrenForDisplay = children;
        let processedChildrenForCopy = children;
        for (const [key, value] of window.rootStore.variableStore.values.entries()) {
          const escapedKey = key.replaceAll(/[.*+?^${}()|[\]\\]/g, (String.raw)`\$&`);
          let displayValue = value;
          if (key === "{yourClientSecret}" && value !== "{yourClientSecret}") {
            displayValue = value.substring(0, 3) + "*****MASKED*****";
          }
          processedChildrenForDisplay = processedChildrenForDisplay.replaceAll(new RegExp(escapedKey, "g"), displayValue);
          processedChildrenForCopy = processedChildrenForCopy.replaceAll(new RegExp(escapedKey, "g"), value);
        }
        setDisplayText(processedChildrenForDisplay);
        setCopyText(processedChildrenForCopy);
      });
    }
    if (window.rootStore) {
      init();
    } else {
      window.addEventListener("adu:storeReady", init);
    }
    return () => {
      window.removeEventListener("adu:storeReady", init);
      unsubscribe?.();
    };
  }, [children]);
  useEffect(() => {
    if (!wrapperRef.current) return;
    const originalWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
    let isOverriding = false;
    const handleClick = e => {
      const button = e.target.closest('[data-testid="copy-code-button"]');
      if (!button || !wrapperRef.current.contains(button)) return;
      isOverriding = true;
      navigator.clipboard.writeText = text => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
          return originalWriteText(copyText);
        }
        return originalWriteText(text);
      };
      setTimeout(() => {
        if (isOverriding) {
          isOverriding = false;
          navigator.clipboard.writeText = originalWriteText;
        }
      }, 100);
    };
    const wrapper = wrapperRef.current;
    wrapper.addEventListener('click', handleClick, true);
    return () => {
      wrapper.removeEventListener('click', handleClick, true);
      if (navigator.clipboard.writeText !== originalWriteText) {
        navigator.clipboard.writeText = originalWriteText;
      }
    };
  }, [copyText]);
  return <div ref={wrapperRef}>
      <CodeBlock filename={filename} icon={icon} language={language} lines highlight={highlight}>
        {displayText}
      </CodeBlock>
    </div>;
};

<HowToSchema />

<Warning>
  `@auth0/auth0-react-router` is currently in **beta** (`1.0.0-beta.2`). The API may change before the stable 1.0 release.
</Warning>

<Accordion title="Use AI to integrate Auth0" icon="microchip-ai" iconType="solid" defaultOpen>
  If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 authentication automatically in minutes using [agent skills](https://agentskills.io/home).

  **Install:**

  ```bash theme={null}
  npx skills add auth0/agent-skills --skill auth0
  ```

  **Then ask your AI assistant:**

  ```text theme={null}
  Add Auth0 authentication to my React Router app
  ```

  Your AI assistant will automatically create your Auth0 application, fetch credentials, install `@auth0/auth0-react-router`, configure the provider, and set up your routes. [Full agent skills documentation →](/docs/quickstart/agent-skills)
</Accordion>

<Callout icon="file-lines" color="#0EA5E9" iconType="regular">
  **Prerequisites:** Before you begin, ensure you have the following installed:

  * **[Node.js](https://nodejs.org/en/download)** 18 or newer (20 LTS recommended)
  * **[npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)** 9+, **[yarn](https://classic.yarnpkg.com/lang/en/docs/install/)** 1.22+, or **[pnpm](https://pnpm.io/installation)** 8+
  * **[jq](https://jqlang.org/)** - Required for Auth0 CLI setup
  * React Router framework mode, v7 or later (`react-router.config.ts` present)
</Callout>

## Get Started

This quickstart demonstrates how to add Auth0 authentication to a React Router application. You'll build a secure app with login, logout, and user profile features using the Auth0 React Router SDK. The SDK handles the OIDC flow server-side and stores the session in a JWE-encrypted cookie — tokens never reach the browser.

export function generateRandomString(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  return Array.from({
    length
  }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}

export const envSnippet = `AUTH0_DOMAIN={yourDomain}
AUTH0_CLIENT_ID={yourClientId}
AUTH0_CLIENT_SECRET={yourClientSecret}
AUTH0_SESSION_SECRET=${generateRandomString(32)}
AUTH0_APP_BASE_URL=http://localhost:5173`;

<Steps>
  <Step title="Create a new React Router project" stepNumber={1}>
    Create a new React Router project for this Quickstart:

    ```shellscript theme={null}
    npx create-react-router@latest my-app
    ```

    Open the project:

    ```shellscript theme={null}
    cd my-app && npm install
    ```

    <Note>
      Skip this step if you are adding Auth0 to an existing React Router app.
    </Note>
  </Step>

  <Step title="Install the Auth0 React Router SDK" stepNumber={2}>
    ```shellscript theme={null}
    npm install @auth0/auth0-react-router
    ```
  </Step>

  <Step title="Configure Auth0" stepNumber={3}>
    Create an Auth0 application and set the callback and logout URLs.

    <Tabs>
      <Tab title="Quick Setup">
        <CreateInteractiveApp type="regular_web" callbackUrl="http://localhost:5173/auth/callback" logoutUrl="http://localhost:5173" />
      </Tab>

      <Tab title="CLI">
        Run the following command from your project's root directory to create an Auth0 app and generate a `.env` file:

        <CodeGroup>
          ```shellscript Mac theme={null}
          # Install Auth0 CLI (if not already installed)
          brew tap auth0/auth0-cli && brew install auth0

          # Set up Auth0 app and generate .env file
          auth0 qs setup --app --type regular --framework react-router --port 5173 --name "My React Router App"
          ```

          ```powershell Windows theme={null}
          # Install Auth0 CLI (if not already installed)
          scoop bucket add auth0 https://github.com/auth0/scoop-auth0-cli.git
          scoop install auth0

          # Set up Auth0 app and generate .env file
          auth0 qs setup --app --type regular --framework react-router --port 5173 --name "My React Router App"
          ```
        </CodeGroup>

        <Note>
          This command will create an Auth0 Regular Web Application configured for `http://localhost:5173` and generate a `.env` file with all required credentials. Skip step 4 if you use this option.
        </Note>
      </Tab>

      <Tab title="Dashboard">
        1. Go to [Auth0 Dashboard](https://manage.auth0.com/) → **Applications > Applications**
        2. Click **Create Application**, enter a name, and select **Regular Web Application**
        3. Click **Create**
        4. Open the **Settings** tab (Application Settings)
        5. Set the following fields:

        | Field                 | Value                                 |
        | --------------------- | ------------------------------------- |
        | Allowed Callback URLs | `http://localhost:5173/auth/callback` |
        | Allowed Logout URLs   | `http://localhost:5173`               |

        6. Click **Save Changes**
        7. Copy **Domain** and **Client ID** from Application Settings — you will use them in the next step
      </Tab>
    </Tabs>
  </Step>

  <Step title="Configure environment variables" stepNumber={4}>
    Create a `.env` file at the root of your project:

    <AuthCodeBlock children={envSnippet} language="shellscript" filename=".env" />

    | Variable               | Where to find it                                                    |
    | ---------------------- | ------------------------------------------------------------------- |
    | `AUTH0_DOMAIN`         | Application Settings → Domain                                       |
    | `AUTH0_CLIENT_ID`      | Application Settings → Client ID                                    |
    | `AUTH0_CLIENT_SECRET`  | Application Settings → Client Secret                                |
    | `AUTH0_SESSION_SECRET` | A random 32+ character string — auto-generated in the snippet above |
    | `AUTH0_APP_BASE_URL`   | Your app's base URL — `http://localhost:5173` for local development |

    <Warning>
      Never commit `.env` to version control. Add it to `.gitignore` before your first commit.
    </Warning>
  </Step>

  <Step title="Create the Auth0 server instance" stepNumber={5}>
    Create `app/auth0.server.ts`. The `.server.ts` suffix tells React Router's bundler to exclude this file from the client bundle, keeping your secrets server-only.

    ```ts app/auth0.server.ts theme={null}
    import { Auth0Server, registerAuth0Instance } from '@auth0/auth0-react-router/server';

    export const auth0 = new Auth0Server();
    registerAuth0Instance(auth0);
    ```
  </Step>

  <Step title="Add the auth routes" stepNumber={6}>
    Create a splat route that handles all `/auth/*` paths. `handleAuth` dispatches internally to `handleLogin`, `handleCallback`, `handleLogout`, and `handleBackchannelLogout` based on the URL path and HTTP method.

    ```tsx app/routes/auth.$.tsx theme={null}
    import { handleAuth } from '@auth0/auth0-react-router/server';
    import { auth0 } from '../auth0.server';

    export const loader = ({ request }: { request: Request }) =>
      handleAuth(auth0, request);

    export const action = ({ request }: { request: Request }) =>
      handleAuth(auth0, request);
    ```

    Register the route in your route config:

    ```ts app/routes.ts theme={null}
    import { type RouteConfig, route } from '@react-router/dev/routes';

    export default [
      route('auth/*', 'routes/auth.$.tsx'),
      // ... your other routes
    ] satisfies RouteConfig;
    ```
  </Step>

  <Step title="Configure the root layout" stepNumber={7}>
    Add `Auth0Provider` and `rootAuthLoader` to `app/root.tsx`. `rootAuthLoader` decrypts the session cookie and passes the auth state to the provider — no tokens are sent to the browser.

    ```tsx app/root.tsx expandable theme={null}
    import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
    import { Auth0Provider } from '@auth0/auth0-react-router';
    import { rootAuthLoader } from '@auth0/auth0-react-router/server';
    import type { Route } from './+types/root';

    export const loader = ({ request }: Route.LoaderArgs) =>
      rootAuthLoader(request);

    export default function Root() {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <Meta />
            <Links />
          </head>
          <body>
            <Auth0Provider>
              <Outlet />
            </Auth0Provider>
            <ScrollRestoration />
            <Scripts />
          </body>
        </html>
      );
    }
    ```

    <Note>
      `Auth0Provider` reads session data from `useRouteLoaderData('root')`, so the root route must have the id `root`. With file-based routing React Router sets this from the filename automatically. With a custom route config, pass `{ id: 'root' }` to the `layout()` call.
    </Note>
  </Step>

  <Step title="Add login and logout" stepNumber={8}>
    Use the built-in components to show login and logout controls. `LoginButton` redirects to `/auth/login` and `LogoutButton` redirects to `/auth/logout`. Auth0 handles the OIDC flow and redirects the user back to your app after sign-in.

    ```tsx app/routes/_index.tsx theme={null}
    import {
      AuthLoading,
      LoginButton,
      LogoutButton,
      SignedIn,
      SignedOut,
    } from '@auth0/auth0-react-router';

    export default function Home() {
      return (
        <main>
          <h1>Welcome</h1>
          <AuthLoading>
            <p>Loading…</p>
          </AuthLoading>
          <SignedOut>
            <LoginButton>Log in</LoginButton>
          </SignedOut>
          <SignedIn>
            <LogoutButton>Log out</LogoutButton>
          </SignedIn>
        </main>
      );
    }
    ```
  </Step>

  <Step title="Show the user profile" stepNumber={9}>
    Use the `useUser` hook to access the authenticated user's profile in any client component. Pair it with `requireSession` in the loader to block unauthenticated requests at the server before the page renders.

    ```tsx app/routes/profile.tsx theme={null}
    import { useUser } from '@auth0/auth0-react-router';
    import { requireSession } from '@auth0/auth0-react-router/server';
    import type { Route } from './+types/profile';

    export const loader = async ({ request }: Route.LoaderArgs) => {
      await requireSession(request);
      return null;
    };

    export default function Profile() {
      const user = useUser();

      return (
        <main>
          <h1>Profile</h1>
          {user && (
            <div>
              <img src={user.picture} alt={user.name ?? ''} width={80} height={80} />
              <p><strong>{user.name}</strong></p>
              <p>{user.email}</p>
            </div>
          )}
        </main>
      );
    }
    ```
  </Step>

  <Step title="Run your application" stepNumber={10}>
    ```shellscript theme={null}
    npm run dev
    ```

    Open `http://localhost:5173` in your browser and click **Log in**. You will be redirected to the Auth0 Universal Login page. After signing in you will be redirected back to your app.
  </Step>
</Steps>

<Check>
  Your app now has working login and logout. The session is stored in a JWE-encrypted cookie — access tokens stay on the server and are never sent to the browser.
</Check>

## Troubleshooting

<AccordionGroup>
  <Accordion title="JWEDecryptionFailed — session cookie cannot be decrypted">
    **Cause:** `AUTH0_SESSION_SECRET` changed after a session cookie was issued, or the value is fewer than 32 characters.

    **Fix:** Clear your browser cookies for `localhost`, confirm `AUTH0_SESSION_SECRET` is at least 32 characters, and restart the dev server. To generate a new secret: `openssl rand -hex 32`.
  </Accordion>

  <Accordion title="Callback URL mismatch — Auth0 returns an error after login">
    **Cause:** The redirect URL Auth0 receives does not match any value in Allowed Callback URLs.

    **Fix:** In [Auth0 Dashboard](https://manage.auth0.com/) → **Applications > Applications** → select your app → **Application Settings**, confirm **Allowed Callback URLs** is set to `http://localhost:5173/auth/callback`. Remove any trailing slashes or extra whitespace, then click **Save Changes**.
  </Accordion>

  <Accordion title="404 on /auth/login — route not found">
    **Cause:** The `auth.$.tsx` splat route is missing or not registered in `routes.ts`.

    **Fix:** Confirm `app/routes/auth.$.tsx` exists and that `app/routes.ts` includes `route('auth/*', 'routes/auth.$.tsx')`. Restart the dev server after editing `routes.ts`.
  </Accordion>

  <Accordion title="useUser returns null after login">
    **Cause:** `rootAuthLoader` is not exported from `app/root.tsx`, or the root route does not have the id `root`.

    **Fix:** Confirm `app/root.tsx` exports `export const loader = ({ request }) => rootAuthLoader(request)`. With a custom route config, register the root layout as `layout('root.tsx', { id: 'root' }, [...routes])`.
  </Accordion>

  <Accordion title="TypeError on context.get — middleware not working">
    **Cause:** `defineRouteAuth` and `auth0Middleware` require React Router 7.9.0 or later, which introduced the middleware API.

    **Fix:** Upgrade `react-router` to `>=7.9.0`, or protect routes individually using `requireSession` / `requireUser` in each loader instead.
  </Accordion>
</AccordionGroup>

## Advanced Usage

<AccordionGroup>
  <Accordion title="Call a backend API with an access token">
    Add `AUTH0_AUDIENCE` to `.env` with your API's identifier (from [Auth0 Dashboard](https://manage.auth0.com/) → **Applications > APIs** → **API Settings → Identifier**). Then use `getAccessToken` in a loader — the token never reaches the browser:

    ```ts app/routes/data.tsx theme={null}
    import { getAccessToken } from '@auth0/auth0-react-router/server';
    import { TokenError } from '@auth0/auth0-react-router/errors';

    export const loader = async ({ request }) => {
      let token: string;
      try {
        token = await getAccessToken(request);
      } catch (err) {
        if (err instanceof TokenError) {
          return new Response(null, { status: 302, headers: { Location: '/auth/login' } });
        }
        throw err;
      }
      const data = await fetch('https://api.example.com/items', {
        headers: { Authorization: `Bearer ${token}` },
      }).then(r => r.json());
      return { data };
    };
    ```
  </Accordion>

  <Accordion title="Role-based route protection">
    Use `defineRouteAuth` middleware (React Router ≥ 7.9.0) to enforce roles at the route level. Roles are read from the `https://auth0.com/claims/roles` claim by default:

    ```ts app/routes/admin.tsx theme={null}
    import { defineRouteHandle } from '@auth0/auth0-react-router';
    import { defineRouteAuth, auth0UserContext } from '@auth0/auth0-react-router/server';

    export const handle = defineRouteHandle({ role: 'admin' });

    export const middleware = defineRouteAuth({ role: 'admin' }).middleware;

    export const loader = ({ context }) => {
      const user = context.get(auth0UserContext);
      return { user };
    };
    ```

    Requests without the required role receive a `403`.
  </Accordion>

  <Accordion title="SPA mode (client-side PKCE)">
    The SDK can run in a purely client-side mode backed by `@auth0/auth0-spa-js`. Add `VITE_AUTH0_DOMAIN` and `VITE_AUTH0_CLIENT_ID` to your `.env` — `Auth0Provider` detects these automatically and switches to the PKCE flow. No other code changes are required.

    ```shellscript theme={null}
    # .env additions for SPA mode
    VITE_AUTH0_DOMAIN={yourDomain}
    VITE_AUTH0_CLIENT_ID={yourClientId}
    ```

    <Warning>
      Do not set both `AUTH0_*` and `VITE_AUTH0_*` variables at the same time. Hybrid mode is not supported — when both are present, SPA logout will not clear the server-side session cookie.
    </Warning>
  </Accordion>
</AccordionGroup>
