import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from 'react'; import { Auth0Client, Auth0ClientOptions, PopupLoginOptions, PopupConfigOptions, GetTokenWithPopupOptions, RedirectLoginResult, GetTokenSilentlyOptions, User, RedirectConnectAccountOptions, ConnectAccountRedirectResult, ResponseType, CustomTokenExchangeOptions, TokenEndpointResponse } from '@auth0/auth0-spa-js'; import Auth0Context, { Auth0ContextInterface, LogoutOptions, RedirectLoginOptions, } from './auth0-context'; import { hasAuthParams, loginError, tokenError, deprecateRedirectUri, } from './utils'; import { reducer } from './reducer'; import { initialAuthState, type AuthState } from './auth-state'; /** * The account that has been connected during the connect flow. */ export type ConnectedAccount = Omit; /** * The state of the application before the user was redirected to the login page * and any account that the user may have connected to. */ export type AppState = { returnTo?: string; connectedAccount?: ConnectedAccount; response_type?: ResponseType; [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; /** * The main configuration to instantiate the `Auth0Provider`. */ export interface Auth0ProviderOptions extends Auth0ClientOptions { /** * The child nodes your Provider has wrapped */ children?: React.ReactNode; /** * By default this removes the code and state parameters from the url when you are redirected from the authorize page. * It uses `window.history` but you might want to overwrite this if you are using a custom router, like `react-router-dom` * See the EXAMPLES.md for more info. */ onRedirectCallback?: (appState?: AppState, user?: TUser) => void; /** * By default, if the page url has code/state params, the SDK will treat them as Auth0's and attempt to exchange the * code for a token. In some cases the code might be for something else (another OAuth SDK perhaps). In these * instances you can instruct the client to ignore them eg * * ```jsx * * ``` */ skipRedirectCallback?: boolean; /** * Context to be used when creating the Auth0Provider, defaults to the internally created context. * * This allows multiple Auth0Providers to be nested within the same application, the context value can then be * passed to useAuth0, withAuth0, or withAuthenticationRequired to use that specific Auth0Provider to access * auth state and methods specifically tied to the provider that the context belongs to. * * When using multiple Auth0Providers in a single application you should do the following to ensure sessions are not * overwritten: * * * Configure a different redirect_uri for each Auth0Provider, and set skipRedirectCallback for each provider to ignore * the others redirect_uri * * If using localstorage for both Auth0Providers, ensure that the audience and scope are different for so that the key * used to store data is different * * 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>; } /** * Replaced by the package version at build time. * @ignore */ declare const __VERSION__: string; /** * @ignore */ const toAuth0ClientOptions = ( opts: Auth0ProviderOptions ): Auth0ClientOptions => { deprecateRedirectUri(opts); return { ...opts, auth0Client: { name: 'auth0-react', version: __VERSION__, }, }; }; /** * @ignore */ const defaultOnRedirectCallback = (appState?: AppState): void => { window.history.replaceState( {}, document.title, appState!.returnTo ?? window.location.pathname ); }; /** * ```jsx * * * * ``` * * Provides the Auth0Context to its child components. */ const Auth0Provider = (opts: Auth0ProviderOptions) => { const { children, skipRedirectCallback, onRedirectCallback = defaultOnRedirectCallback, context = Auth0Context, ...clientOpts } = opts; const [client] = useState( () => new Auth0Client(toAuth0ClientOptions(clientOpts)) ); const [state, dispatch] = useReducer(reducer, initialAuthState as AuthState); const didInitialise = useRef(false); const handleError = useCallback((error: Error) => { dispatch({ type: 'ERROR', error }); return error; }, []); useEffect(() => { if (didInitialise.current) { return; } didInitialise.current = true; (async (): Promise => { try { let user: TUser | undefined; if (hasAuthParams() && !skipRedirectCallback) { const { appState = {}, response_type, ...result } = await client.handleRedirectCallback(); user = await client.getUser(); appState.response_type = response_type; if (response_type === ResponseType.ConnectCode) { appState.connectedAccount = result as ConnectedAccount; } onRedirectCallback(appState, user); } else { await client.checkSession(); user = await client.getUser(); } dispatch({ type: 'INITIALISED', user }); } catch (error) { handleError(loginError(error)); } })(); }, [client, onRedirectCallback, skipRedirectCallback, handleError]); const loginWithRedirect = useCallback( (opts?: RedirectLoginOptions): Promise => { deprecateRedirectUri(opts); return client.loginWithRedirect(opts); }, [client] ); const loginWithPopup = useCallback( async ( options?: PopupLoginOptions, config?: PopupConfigOptions ): Promise => { dispatch({ type: 'LOGIN_POPUP_STARTED' }); try { await client.loginWithPopup(options, config); } catch (error) { handleError(loginError(error)); return; } const user = await client.getUser(); dispatch({ type: 'LOGIN_POPUP_COMPLETE', user }); }, [client, handleError] ); const logout = useCallback( async (opts: LogoutOptions = {}): Promise => { await client.logout(opts); if (opts.openUrl || opts.openUrl === false) { dispatch({ type: 'LOGOUT' }); } }, [client] ); const getAccessTokenSilently = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any async (opts?: GetTokenSilentlyOptions): Promise => { let token; try { token = await client.getTokenSilently(opts); } catch (error) { throw tokenError(error); } finally { dispatch({ type: 'GET_ACCESS_TOKEN_COMPLETE', user: await client.getUser(), }); } return token; }, [client] ); const getAccessTokenWithPopup = useCallback( async ( opts?: GetTokenWithPopupOptions, config?: PopupConfigOptions ): Promise => { let token; try { token = await client.getTokenWithPopup(opts, config); } catch (error) { throw tokenError(error); } finally { dispatch({ type: 'GET_ACCESS_TOKEN_COMPLETE', user: await client.getUser(), }); } return token; }, [client] ); const connectAccountWithRedirect = useCallback( (options: RedirectConnectAccountOptions) => client.connectAccountWithRedirect(options), [client] ); const getIdTokenClaims = useCallback( () => client.getIdTokenClaims(), [client] ); const loginWithCustomTokenExchange = useCallback( async ( options: CustomTokenExchangeOptions ): Promise => { let tokenResponse; try { tokenResponse = await client.loginWithCustomTokenExchange(options); } catch (error) { throw tokenError(error); } finally { // We dispatch the standard GET_ACCESS_TOKEN_COMPLETE action here to maintain // backward compatibility and consistency with the getAccessTokenSilently flow. // This ensures the SDK's internal state lifecycle (loading/user updates) remains // identical regardless of whether the token was retrieved via silent auth or CTE. dispatch({ type: 'GET_ACCESS_TOKEN_COMPLETE', user: await client.getUser(), }); } return tokenResponse; }, [client] ); const exchangeToken = useCallback( async ( options: CustomTokenExchangeOptions ): Promise => { return loginWithCustomTokenExchange(options); }, [loginWithCustomTokenExchange] ); const handleRedirectCallback = useCallback( async ( url?: string ): Promise => { try { return await client.handleRedirectCallback(url); } catch (error) { throw tokenError(error); } finally { dispatch({ type: 'HANDLE_REDIRECT_COMPLETE', user: await client.getUser(), }); } }, [client] ); const getDpopNonce = useCallback( (id) => client.getDpopNonce(id), [client] ); const setDpopNonce = useCallback( (nonce, id) => client.setDpopNonce(nonce, id), [client] ); const generateDpopProof = useCallback( (params) => client.generateDpopProof(params), [client] ); const createFetcher = useCallback( (config) => client.createFetcher(config), [client] ); const getConfiguration = useCallback( () => client.getConfiguration(), [client] ); const mfa = useMemo(() => client.mfa, [client]); const contextValue = useMemo>(() => { return { ...state, getAccessTokenSilently, getAccessTokenWithPopup, getIdTokenClaims, loginWithCustomTokenExchange, exchangeToken, loginWithRedirect, loginWithPopup, connectAccountWithRedirect, logout, handleRedirectCallback, getDpopNonce, setDpopNonce, generateDpopProof, createFetcher, getConfiguration, mfa, }; }, [ state, getAccessTokenSilently, getAccessTokenWithPopup, getIdTokenClaims, loginWithCustomTokenExchange, exchangeToken, loginWithRedirect, loginWithPopup, connectAccountWithRedirect, logout, handleRedirectCallback, getDpopNonce, setDpopNonce, generateDpopProof, createFetcher, getConfiguration, mfa, ]); return {children}; }; export default Auth0Provider;