Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
},
});
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may also lead to inconsistent api behaviour, for example

Someone who creates a rawClient would need to call

export const auth0Client = createAuth0Client({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
});


//called on button click
client.getTokenSilently({ ... })

vs

Call silent token token via auth0 hook

var auth0 = useAuth0();

//called on button click
auth0.getAccessTokenSilently({ cacheMode: 'off' })

Should we highlight this potential mismatch.

Additionally for anyone using the raw auth0 client via createAuth0Client(...) will internal state in react sdk created in https://github.com/auth0/auth0-react/blob/main/src/reducer.tsx will be updated ?
In case they are not, will there be potential side effects ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note on both points: the method naming difference (getTokenSilently vs getAccessTokenSilently), the state sync behaviour, and a callout to avoid calling client.logout() directly on the raw client.


Pass the client to `Auth0Provider`:

```jsx
import { Auth0Provider } from '@auth0/auth0-react';
import { auth0Client } from './auth0-client';

export default function App() {
return (
<Auth0Provider client={auth0Client}>
<MyApp />
</Auth0Provider>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should highlight potential pitfalls and anti patterns with this approach.

What happens if a user initilaise Auth0Provider with both raw client and together

export const auth0Client = createAuth0Client({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
});
 <Auth0Provider
              client={auth0Client}
              domain={config.domain}
              clientId={config.clientId}
              key={JSON.stringify(config)}
              authorizationParams={{
                redirect_uri: window.location.origin,
                audience: config.audience || 'default'
              }}
 ...
            >
            </Auth0Provider>

I believe, currently this is allowed, should we add checks to restrict this behaviour or at-least document this as an anti pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Added a runtime console.warn for this case. TypeScript users are already covered at compile time via client?: never on Auth0ProviderWithConfigOptions.

```

> **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):
Expand Down
36 changes: 35 additions & 1 deletion __tests__/auth-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Auth0ProviderOptions>);
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(
Expand Down
12 changes: 6 additions & 6 deletions __tests__/helpers.tsx
Original file line number Diff line number Diff line change
@@ -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<Auth0ProviderOptions> = {}) => {
export const createWrapper = (opts: Partial<Auth0ProviderOptions> = {}) => {
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<Record<string, unknown>>): React.JSX.Element {
return (
<Auth0Provider domain={domain} clientId={clientId} {...opts}>
<Auth0Provider {...providerProps}>
{children}
</Auth0Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions examples/cra-react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Omit<Auth0ProviderOptions, 'context'>> & {
}: PropsWithChildren<Omit<Auth0ProviderWithConfigOptions, 'context'>> & {
context?: React.Context<Auth0ContextInterface<User>>
}) => {
const navigate = useNavigate();
Expand Down
44 changes: 36 additions & 8 deletions src/auth0-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TUser extends User = User> extends Auth0ClientOptions {
type Auth0ProviderBaseOptions<TUser extends User = User> = {
/**
* The child nodes your Provider has wrapped
*/
Expand Down Expand Up @@ -97,7 +94,30 @@ export interface Auth0ProviderOptions<TUser extends User = User> 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<Auth0ContextInterface<TUser>>;
}
};

/**
* Options for `Auth0Provider` when configuring Auth0 via `domain` and `clientId`.
* Use this type when building wrapper components around `Auth0Provider`.
*/
export type Auth0ProviderWithConfigOptions<TUser extends User = User> =
Auth0ProviderBaseOptions<TUser> & Auth0ClientOptions & { client?: never };

/**
* Options for `Auth0Provider` when supplying a pre-configured `Auth0Client` instance.
*/
export type Auth0ProviderWithClientOptions<TUser extends User = User> =
Auth0ProviderBaseOptions<TUser> & { 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<TUser extends User = User> =
| Auth0ProviderWithConfigOptions<TUser>
| Auth0ProviderWithClientOptions<TUser>;

/**
* Replaced by the package version at build time.
Expand All @@ -109,7 +129,7 @@ declare const __VERSION__: string;
* @ignore
*/
const toAuth0ClientOptions = (
opts: Auth0ProviderOptions
opts: Auth0ClientOptions
): Auth0ClientOptions => {
deprecateRedirectUri(opts);

Expand Down Expand Up @@ -151,10 +171,18 @@ const Auth0Provider = <TUser extends User = User>(opts: Auth0ProviderOptions<TUs
skipRedirectCallback,
onRedirectCallback = defaultOnRedirectCallback,
context = Auth0Context,
client: providedClient,
...clientOpts
} = opts;
} = opts as Auth0ProviderBaseOptions<TUser> & 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<TUser>, initialAuthState as AuthState<TUser>);
const didInitialise = useRef(false);
Expand Down
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export {
default as Auth0Provider,
Auth0ProviderOptions,
Auth0ProviderWithConfigOptions,
Auth0ProviderWithClientOptions,
AppState,
ConnectedAccount
} from './auth0-provider';
Expand Down
Loading