Search code examples
typescriptreact-routerauth0

How do you get an Auth0 access token in a React Router v6.4 loader and action?


How do you get access to an Auth0 access token inside React Router v6.4 actions and loaders so you can call a secured API?

I've tried to curry the getAccessTokenSilently() method to the loader but I can't call the useAuth0() hook outside a component (my routes are created outside a component as recommended by the react router documentation). I can't call useAuth0() inside the loader for the same reason: it's not a component.

I tried putting the routes inside a component anyway (moving the routes shown in my code below to an <App/> component) but that results in errors from useAuth0() about it having to be called from within an Auth0Provider.

The official Auth0 guide to React Router v6 hasn't been updated to use createBrowserRouter and doesn't use loaders in their secure API example.

This seems like a fundamental need in any React Router v6.4 application and yet I've never been able to find an example that shows how to do it. Surely someone has done this before?

Update: Thanks to the assistance of Drew this works! The below code is a full working version of this.

Auth0ProviderWithNavigate.tsx

interface Auth0ProviderWithNavigateProps {
  children: React.ReactNode;
}

export const Auth0ProviderWithNavigate = ({
  children,
}: Auth0ProviderWithNavigateProps) => {
  if (
    !(
      ENV.VITE_AUTH0_DOMAIN &&
      ENV.VITE_AUTH0_CLIENT_ID &&
      ENV.VITE_AUTH0_CALLBACK_URL
    )
  ) {
    return null;
  }

  return (
    <Auth0Provider
      domain={ENV.VITE_AUTH0_DOMAIN}
      clientId={ENV.VITE_AUTH0_CLIENT_ID}
      authorizationParams={{
        redirect_uri: ENV.VITE_AUTH0_CALLBACK_URL,
      }}
    >
      {children}
    </Auth0Provider>
  );
};

AuthenticationGuard.tsx

import { withAuthenticationRequired } from "@auth0/auth0-react";
import { PageLoader } from "./PageLoader";

interface AuthenticationGuardProps {
  component: React.ComponentType<object>;
}

export const AuthenticationGuard = ({
  component,
}: AuthenticationGuardProps) => {
  const Component = withAuthenticationRequired(component, {
    onRedirecting: () => <PageLoader />,
  });

  return <Component />;
};

main.tsx

const root = document.getElementById("root");

root &&
  ReactDOM.createRoot(root).render(
    <React.StrictMode>
      <AppContextProvider>
        <Auth0ProviderWithNavigate>
          <AppTheme>
            <App />
          </AppTheme>
        </Auth0ProviderWithNavigate>
      </AppContextProvider>
    </React.StrictMode>
  );

App.tsx

const App = () => {
  const { getAccessTokenSilently } = useAuth0();

  const router = useMemo(() => {
    return createBrowserRouter([
      {
        children: [
          {
            path: "/",
            element: <WelcomePage />,
            errorElement: <ErrorPage />,
          },
          {
            children: [
              {
                id: "edct",
                path: "/edct",
                element: <AuthenticationGuard component={Edct} />,
                errorElement: <ErrorPage />,
                loader: AirportCodesLoader({ getAccessTokenSilently }),
              },
            ],
          },
        ],
      },
    ]);
  }, [getAccessTokenSilently]);

  return <RouterProvider router={router} />;
};

export default App;

AirportCodesLoader.tsx

import { ActionFunction } from "react-router";

const cleanCodes = (codes: string | null): string | null => {
  if (!codes) {
    return null;
  }
  return codes
    .split(",")
    .map((code) => code.trim())
    .join(",");
};

interface AppLoader {
  getAccessTokenSilently: () => Promise<string>;
}

export const AirportCodesLoader =
  ({ getAccessTokenSilently }: AppLoader): ActionFunction =>
  async ({ request }) => {
    const url = new URL(request.url);

    console.log(await getAccessTokenSilently());
    return {
      departureCodes: cleanCodes(url.searchParams.get("departureCodes")) ?? "",
      arrivalCodes: cleanCodes(url.searchParams.get("arrivalCodes")) ?? "",
    };
  };

Solution

  • Yes, while it is the common practice to declare the router configuration outside the React tree, this is just a recommendation. In this case you can create it inside the App component so it can close over an instance of the auth0 context value to be passed to route loaders. The route loaders that need to receive the auth0 context will need to curry that parameter.

    Example:

    const AirportCodesLoader = (auth0) => ({ params, request }) => {
      ... loader logic ...
    };
    
    const App = () => {
      const auth0 = useAuth0();
    
      const router = React.useMemo(() => { // <-- memoize router reference
        return createBrowserRouter([
          {
            children: [
              {
                path: "/",
                element: <WelcomePage />,
                errorElement: <ErrorPage />,
              },
              {
                children: [
                  {
                    id: "edct",
                    path: "/edct",
                    element: <AuthenticationGuard component={Edct} />,
                    errorElement: <ErrorPage />,
                    loader: AirportCodesLoader(auth0), // <-- pass auth0 to loader
                  },
                ],
              },
            ],
          },
        ]);
      }, [auth0]); // <-- auth0 external dependency
    
      return <RouterProvider router={router} />;
    };