Skip to content

Commit a63eb39

Browse files
feat: add client prop to Auth0Provider (#1041)
## What Adds a `client` prop to `Auth0Provider` so a pre-configured `Auth0Client` instance can be shared between the React tree and code outside of React's lifecycle (e.g. TanStack Start client function middleware). Closes #1037. Supersedes #849 (thanks to @rodrigowbazevedo for the original draft). ## Usage ```tsx import { Auth0Client } from '@auth0/auth0-spa-js'; import { Auth0Provider } from '@auth0/auth0-react'; const client = new Auth0Client({ domain, clientId }); // Outside React (e.g. TanStack Start client function middleware) // Note: the raw Auth0Client method is getTokenSilently (not getAccessTokenSilently) const token = await client.getTokenSilently(); // Inside React — Auth0Provider uses the same client instance function App() { return <Auth0Provider client={client}><MyApp /></Auth0Provider>; } ``` ## Notes - Fully backward compatible — existing usage without `client` prop is unchanged - Calling methods on the raw client does not update React state — use hooks inside React for that
1 parent 5116ee4 commit a63eb39

6 files changed

Lines changed: 136 additions & 17 deletions

File tree

EXAMPLES.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- [Use with a Class Component](#use-with-a-class-component)
44
- [Protect a Route](#protect-a-route)
55
- [Call an API](#call-an-api)
6+
- [Use Auth0 outside of React](#use-auth0-outside-of-react)
67
- [Protecting a route in a `react-router-dom v6` app](#protecting-a-route-in-a-react-router-dom-v6-app)
78
- [Protecting a route in a Gatsby app](#protecting-a-route-in-a-gatsby-app)
89
- [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 = () => {
102103
export default Posts;
103104
```
104105

106+
## Use Auth0 outside of React
107+
108+
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.
109+
110+
```jsx
111+
// auth0-client.js
112+
import { Auth0Client } from '@auth0/auth0-spa-js';
113+
114+
export const auth0Client = new Auth0Client({
115+
domain: 'YOUR_AUTH0_DOMAIN',
116+
clientId: 'YOUR_AUTH0_CLIENT_ID',
117+
authorizationParams: {
118+
redirect_uri: window.location.origin,
119+
},
120+
});
121+
```
122+
123+
Pass the client to `Auth0Provider`:
124+
125+
```jsx
126+
import { Auth0Provider } from '@auth0/auth0-react';
127+
import { auth0Client } from './auth0-client';
128+
129+
export default function App() {
130+
return (
131+
<Auth0Provider client={auth0Client}>
132+
<MyApp />
133+
</Auth0Provider>
134+
);
135+
}
136+
```
137+
138+
> **Note:**
139+
> - The raw `Auth0Client` method is `getTokenSilently()`, not `getAccessTokenSilently()`. They share the same token cache but the hook version also updates React state.
140+
> - 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.
141+
142+
Use the same client instance in a TanStack Start client function middleware:
143+
144+
```js
145+
import { createMiddleware } from '@tanstack/react-start';
146+
import { auth0Client } from './auth0-client';
147+
148+
export const authMiddleware = createMiddleware({ type: 'function' }).client(
149+
async ({ next }) => {
150+
const token = await auth0Client.getTokenSilently();
151+
return next({
152+
headers: {
153+
Authorization: `Bearer ${token}`,
154+
},
155+
});
156+
},
157+
);
158+
```
159+
105160
## Custom token exchange
106161

107162
Exchange an external subject token for Auth0 tokens using the token exchange flow (RFC 8693):

__tests__/auth-provider.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import '@testing-library/jest-dom';
77
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
88
import React, { StrictMode, useContext } from 'react';
99
import pkg from '../package.json';
10-
import { Auth0Provider, useAuth0 } from '../src';
10+
import { Auth0Provider, Auth0ProviderOptions, useAuth0 } from '../src';
1111
import Auth0Context, {
1212
Auth0ContextInterface,
1313
initialContext,
@@ -134,6 +134,40 @@ describe('Auth0Provider', () => {
134134
});
135135
});
136136

137+
it('should use provided client instance without creating a new one', async () => {
138+
const wrapper = createWrapper({ client: clientMock });
139+
renderHook(() => useContext(Auth0Context), { wrapper });
140+
await waitFor(() => {
141+
expect(Auth0Client).not.toHaveBeenCalled();
142+
expect(clientMock.checkSession).toHaveBeenCalled();
143+
});
144+
});
145+
146+
it('should warn when client prop is used alongside domain or clientId', async () => {
147+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
148+
const wrapper = createWrapper({ client: clientMock, domain: 'foo', clientId: 'bar' } as unknown as Partial<Auth0ProviderOptions>);
149+
renderHook(() => useContext(Auth0Context), { wrapper });
150+
await waitFor(() => {
151+
expect(warn).toHaveBeenCalledWith(
152+
expect.stringContaining('the `client` prop takes precedence')
153+
);
154+
});
155+
warn.mockRestore();
156+
});
157+
158+
it('should not warn when only client prop is provided', async () => {
159+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
160+
const wrapper = createWrapper({ client: clientMock });
161+
renderHook(() => useContext(Auth0Context), { wrapper });
162+
await waitFor(() => {
163+
expect(clientMock.checkSession).toHaveBeenCalled();
164+
});
165+
expect(warn).not.toHaveBeenCalledWith(
166+
expect.stringContaining('the `client` prop takes precedence')
167+
);
168+
warn.mockRestore();
169+
});
170+
137171
it('should check session when logged out', async () => {
138172
const wrapper = createWrapper();
139173
const { result } = renderHook(

__tests__/helpers.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import React, { PropsWithChildren } from 'react';
22
import Auth0Provider, { Auth0ProviderOptions } from '../src/auth0-provider';
33

4-
export const createWrapper = ({
5-
clientId = '__test_client_id__',
6-
domain = '__test_domain__',
7-
...opts
8-
}: Partial<Auth0ProviderOptions> = {}) => {
4+
export const createWrapper = (opts: Partial<Auth0ProviderOptions> = {}) => {
5+
const providerProps =
6+
'client' in opts && opts.client != null
7+
? (opts as Auth0ProviderOptions)
8+
: ({ clientId: '__test_client_id__', domain: '__test_domain__', ...opts } as Auth0ProviderOptions);
99
return function Wrapper({
1010
children,
1111
}: PropsWithChildren<Record<string, unknown>>): React.JSX.Element {
1212
return (
13-
<Auth0Provider domain={domain} clientId={clientId} {...opts}>
13+
<Auth0Provider {...providerProps}>
1414
{children}
1515
</Auth0Provider>
1616
);

examples/cra-react-router/src/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import React, { PropsWithChildren } from 'react';
33
import App from './App';
44
import { Auth0Provider, AppState, Auth0ContextInterface, User } from '@auth0/auth0-react';
55
import { BrowserRouter, useNavigate } from 'react-router-dom';
6-
import { Auth0ProviderOptions } from '../../../src/index.js';
6+
import { Auth0ProviderWithConfigOptions } from '../../../src/index.js';
77

88
const Auth0ProviderWithRedirectCallback = ({
99
children,
1010
context,
1111
...props
12-
}: PropsWithChildren<Omit<Auth0ProviderOptions, 'context'>> & {
12+
}: PropsWithChildren<Omit<Auth0ProviderWithConfigOptions, 'context'>> & {
1313
context?: React.Context<Auth0ContextInterface<User>>
1414
}) => {
1515
const navigate = useNavigate();

src/auth0-provider.tsx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ export type AppState = {
5151
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
5252
};
5353

54-
/**
55-
* The main configuration to instantiate the `Auth0Provider`.
56-
*/
57-
export interface Auth0ProviderOptions<TUser extends User = User> extends Auth0ClientOptions {
54+
type Auth0ProviderBaseOptions<TUser extends User = User> = {
5855
/**
5956
* The child nodes your Provider has wrapped
6057
*/
@@ -97,7 +94,30 @@ export interface Auth0ProviderOptions<TUser extends User = User> extends Auth0Cl
9794
* 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)
9895
*/
9996
context?: React.Context<Auth0ContextInterface<TUser>>;
100-
}
97+
};
98+
99+
/**
100+
* Options for `Auth0Provider` when configuring Auth0 via `domain` and `clientId`.
101+
* Use this type when building wrapper components around `Auth0Provider`.
102+
*/
103+
export type Auth0ProviderWithConfigOptions<TUser extends User = User> =
104+
Auth0ProviderBaseOptions<TUser> & Auth0ClientOptions & { client?: never };
105+
106+
/**
107+
* Options for `Auth0Provider` when supplying a pre-configured `Auth0Client` instance.
108+
*/
109+
export type Auth0ProviderWithClientOptions<TUser extends User = User> =
110+
Auth0ProviderBaseOptions<TUser> & { client: Auth0Client };
111+
112+
/**
113+
* The main configuration to instantiate the `Auth0Provider`.
114+
*
115+
* Either provide `domain` and `clientId` (`Auth0ProviderWithConfigOptions`)
116+
* or a pre-configured `client` instance (`Auth0ProviderWithClientOptions`).
117+
*/
118+
export type Auth0ProviderOptions<TUser extends User = User> =
119+
| Auth0ProviderWithConfigOptions<TUser>
120+
| Auth0ProviderWithClientOptions<TUser>;
101121

102122
/**
103123
* Replaced by the package version at build time.
@@ -109,7 +129,7 @@ declare const __VERSION__: string;
109129
* @ignore
110130
*/
111131
const toAuth0ClientOptions = (
112-
opts: Auth0ProviderOptions
132+
opts: Auth0ClientOptions
113133
): Auth0ClientOptions => {
114134
deprecateRedirectUri(opts);
115135

@@ -151,10 +171,18 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
151171
skipRedirectCallback,
152172
onRedirectCallback = defaultOnRedirectCallback,
153173
context = Auth0Context,
174+
client: providedClient,
154175
...clientOpts
155-
} = opts;
176+
} = opts as Auth0ProviderBaseOptions<TUser> & Auth0ClientOptions & { client?: Auth0Client };
177+
if (providedClient && (clientOpts.domain || clientOpts.clientId)) {
178+
console.warn(
179+
'Auth0Provider: the `client` prop takes precedence over `domain`/`clientId` and other ' +
180+
'configuration options. Remove `domain`, `clientId`, and any other Auth0Client configuration ' +
181+
'props when using the `client` prop.'
182+
);
183+
}
156184
const [client] = useState(
157-
() => new Auth0Client(toAuth0ClientOptions(clientOpts))
185+
() => providedClient ?? new Auth0Client(toAuth0ClientOptions(clientOpts))
158186
);
159187
const [state, dispatch] = useReducer(reducer<TUser>, initialAuthState as AuthState<TUser>);
160188
const didInitialise = useRef(false);

src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export {
22
default as Auth0Provider,
33
Auth0ProviderOptions,
4+
Auth0ProviderWithConfigOptions,
5+
Auth0ProviderWithClientOptions,
46
AppState,
57
ConnectedAccount
68
} from './auth0-provider';

0 commit comments

Comments
 (0)