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';