diff --git a/EXAMPLES.md b/EXAMPLES.md
index 3c407f9d..38fbdb90 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -3,6 +3,7 @@
- [Use with a Class Component](#use-with-a-class-component)
- [Protect a Route](#protect-a-route)
- [Call an API](#call-an-api)
+- [Use Auth0 outside of React](#use-auth0-outside-of-react)
- [Protecting a route in a `react-router-dom v6` app](#protecting-a-route-in-a-react-router-dom-v6-app)
- [Protecting a route in a Gatsby app](#protecting-a-route-in-a-gatsby-app)
- [Protecting a route in a Next.js app (in SPA mode)](#protecting-a-route-in-a-nextjs-app-in-spa-mode)
@@ -102,6 +103,60 @@ const Posts = () => {
export default Posts;
```
+## Use Auth0 outside of React
+
+If you need to share an `Auth0Client` instance between the React tree and code that has no access to React's lifecycle — such as TanStack Start client function middleware — create an `Auth0Client` and pass it to `Auth0Provider` via the `client` prop.
+
+```jsx
+// auth0-client.js
+import { Auth0Client } from '@auth0/auth0-spa-js';
+
+export const auth0Client = new Auth0Client({
+ domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_AUTH0_CLIENT_ID',
+ authorizationParams: {
+ redirect_uri: window.location.origin,
+ },
+});
+```
+
+Pass the client to `Auth0Provider`:
+
+```jsx
+import { Auth0Provider } from '@auth0/auth0-react';
+import { auth0Client } from './auth0-client';
+
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+> **Note:**
+> - The raw `Auth0Client` method is `getTokenSilently()`, not `getAccessTokenSilently()`. They share the same token cache but the hook version also updates React state.
+> - Calling methods on the raw client does not update React state. For token fetching this is fine since the cache is shared. Avoid calling `client.logout()` directly — use the `logout` method from `useAuth0` instead so React state stays in sync.
+
+Use the same client instance in a TanStack Start client function middleware:
+
+```js
+import { createMiddleware } from '@tanstack/react-start';
+import { auth0Client } from './auth0-client';
+
+export const authMiddleware = createMiddleware({ type: 'function' }).client(
+ async ({ next }) => {
+ const token = await auth0Client.getTokenSilently();
+ return next({
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ },
+);
+```
+
## Custom token exchange
Exchange an external subject token for Auth0 tokens using the token exchange flow (RFC 8693):
diff --git a/__tests__/auth-provider.test.tsx b/__tests__/auth-provider.test.tsx
index ec6163c6..3453f858 100644
--- a/__tests__/auth-provider.test.tsx
+++ b/__tests__/auth-provider.test.tsx
@@ -7,7 +7,7 @@ import '@testing-library/jest-dom';
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
import React, { StrictMode, useContext } from 'react';
import pkg from '../package.json';
-import { Auth0Provider, useAuth0 } from '../src';
+import { Auth0Provider, Auth0ProviderOptions, useAuth0 } from '../src';
import Auth0Context, {
Auth0ContextInterface,
initialContext,
@@ -134,6 +134,40 @@ describe('Auth0Provider', () => {
});
});
+ it('should use provided client instance without creating a new one', async () => {
+ const wrapper = createWrapper({ client: clientMock });
+ renderHook(() => useContext(Auth0Context), { wrapper });
+ await waitFor(() => {
+ expect(Auth0Client).not.toHaveBeenCalled();
+ expect(clientMock.checkSession).toHaveBeenCalled();
+ });
+ });
+
+ it('should warn when client prop is used alongside domain or clientId', async () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
+ const wrapper = createWrapper({ client: clientMock, domain: 'foo', clientId: 'bar' } as unknown as Partial);
+ renderHook(() => useContext(Auth0Context), { wrapper });
+ await waitFor(() => {
+ expect(warn).toHaveBeenCalledWith(
+ expect.stringContaining('the `client` prop takes precedence')
+ );
+ });
+ warn.mockRestore();
+ });
+
+ it('should not warn when only client prop is provided', async () => {
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
+ const wrapper = createWrapper({ client: clientMock });
+ renderHook(() => useContext(Auth0Context), { wrapper });
+ await waitFor(() => {
+ expect(clientMock.checkSession).toHaveBeenCalled();
+ });
+ expect(warn).not.toHaveBeenCalledWith(
+ expect.stringContaining('the `client` prop takes precedence')
+ );
+ warn.mockRestore();
+ });
+
it('should check session when logged out', async () => {
const wrapper = createWrapper();
const { result } = renderHook(
diff --git a/__tests__/helpers.tsx b/__tests__/helpers.tsx
index 821d8547..f081bdd8 100644
--- a/__tests__/helpers.tsx
+++ b/__tests__/helpers.tsx
@@ -1,16 +1,16 @@
import React, { PropsWithChildren } from 'react';
import Auth0Provider, { Auth0ProviderOptions } from '../src/auth0-provider';
-export const createWrapper = ({
- clientId = '__test_client_id__',
- domain = '__test_domain__',
- ...opts
-}: Partial = {}) => {
+export const createWrapper = (opts: Partial = {}) => {
+ const providerProps =
+ 'client' in opts && opts.client != null
+ ? (opts as Auth0ProviderOptions)
+ : ({ clientId: '__test_client_id__', domain: '__test_domain__', ...opts } as Auth0ProviderOptions);
return function Wrapper({
children,
}: PropsWithChildren>): React.JSX.Element {
return (
-
+
{children}
);
diff --git a/examples/cra-react-router/src/index.tsx b/examples/cra-react-router/src/index.tsx
index 3af442b1..0cbe8e9d 100644
--- a/examples/cra-react-router/src/index.tsx
+++ b/examples/cra-react-router/src/index.tsx
@@ -3,13 +3,13 @@ import React, { PropsWithChildren } from 'react';
import App from './App';
import { Auth0Provider, AppState, Auth0ContextInterface, User } from '@auth0/auth0-react';
import { BrowserRouter, useNavigate } from 'react-router-dom';
-import { Auth0ProviderOptions } from '../../../src/index.js';
+import { Auth0ProviderWithConfigOptions } from '../../../src/index.js';
const Auth0ProviderWithRedirectCallback = ({
children,
context,
...props
-}: PropsWithChildren> & {
+}: PropsWithChildren> & {
context?: React.Context>
}) => {
const navigate = useNavigate();
diff --git a/src/auth0-provider.tsx b/src/auth0-provider.tsx
index 7d49d317..c7077d55 100644
--- a/src/auth0-provider.tsx
+++ b/src/auth0-provider.tsx
@@ -51,10 +51,7 @@ export type AppState = {
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
};
-/**
- * The main configuration to instantiate the `Auth0Provider`.
- */
-export interface Auth0ProviderOptions extends Auth0ClientOptions {
+type Auth0ProviderBaseOptions = {
/**
* The child nodes your Provider has wrapped
*/
@@ -97,7 +94,30 @@ export interface Auth0ProviderOptions extends Auth0Cl
* For a sample on using multiple Auth0Providers review the [React Account Linking Sample](https://github.com/auth0-samples/auth0-link-accounts-sample/tree/react-variant)
*/
context?: React.Context>;
-}
+};
+
+/**
+ * Options for `Auth0Provider` when configuring Auth0 via `domain` and `clientId`.
+ * Use this type when building wrapper components around `Auth0Provider`.
+ */
+export type Auth0ProviderWithConfigOptions =
+ Auth0ProviderBaseOptions & Auth0ClientOptions & { client?: never };
+
+/**
+ * Options for `Auth0Provider` when supplying a pre-configured `Auth0Client` instance.
+ */
+export type Auth0ProviderWithClientOptions =
+ Auth0ProviderBaseOptions & { client: Auth0Client };
+
+/**
+ * The main configuration to instantiate the `Auth0Provider`.
+ *
+ * Either provide `domain` and `clientId` (`Auth0ProviderWithConfigOptions`)
+ * or a pre-configured `client` instance (`Auth0ProviderWithClientOptions`).
+ */
+export type Auth0ProviderOptions =
+ | Auth0ProviderWithConfigOptions
+ | Auth0ProviderWithClientOptions;
/**
* Replaced by the package version at build time.
@@ -109,7 +129,7 @@ declare const __VERSION__: string;
* @ignore
*/
const toAuth0ClientOptions = (
- opts: Auth0ProviderOptions
+ opts: Auth0ClientOptions
): Auth0ClientOptions => {
deprecateRedirectUri(opts);
@@ -151,10 +171,18 @@ const Auth0Provider = (opts: Auth0ProviderOptions & Auth0ClientOptions & { client?: Auth0Client };
+ if (providedClient && (clientOpts.domain || clientOpts.clientId)) {
+ console.warn(
+ 'Auth0Provider: the `client` prop takes precedence over `domain`/`clientId` and other ' +
+ 'configuration options. Remove `domain`, `clientId`, and any other Auth0Client configuration ' +
+ 'props when using the `client` prop.'
+ );
+ }
const [client] = useState(
- () => new Auth0Client(toAuth0ClientOptions(clientOpts))
+ () => providedClient ?? new Auth0Client(toAuth0ClientOptions(clientOpts))
);
const [state, dispatch] = useReducer(reducer, initialAuthState as AuthState);
const didInitialise = useRef(false);
diff --git a/src/index.tsx b/src/index.tsx
index 4db5b72e..2c635503 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,8 @@
export {
default as Auth0Provider,
Auth0ProviderOptions,
+ Auth0ProviderWithConfigOptions,
+ Auth0ProviderWithClientOptions,
AppState,
ConnectedAccount
} from './auth0-provider';