diff --git a/FAQ.md b/FAQ.md
index 6db02a8e..76d91808 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -4,6 +4,7 @@
1. [User is not logged in after page refresh](#1-user-is-not-logged-in-after-page-refresh)
2. [User is not logged in after successful sign in with redirect](#2-user-is-not-logged-in-after-successful-sign-in-with-redirect)
+3. [How to handle authentication in Electron or other native applications using react](#3-how-to-handle-authentication-in-electron-or-other-native-applications)
## 1. User is not logged in after page refresh
@@ -20,3 +21,38 @@ In this case Silent Authentication will not work because it relies on a hidden i
## 2. User is not logged in after successful sign in with redirect
If after successfully logging in, your user returns to your SPA and is still not authenticated, do _not_ refresh the page - go to the Network tab on Chrome and confirm that the POST to `oauth/token` resulted in an error `401 Unauthorized`. If this is the case, your tenant is most likely misconfigured. Go to your **Application Properties** in your application's settings in the [Auth0 Dashboard](https://manage.auth0.com) and make sure that `Application Type` is set to `Single Page Application` and `Token Endpoint Authentication Method` is set to `None` (**Note:** there is a known issue with the Auth0 "Default App", if you are unable to set `Token Endpoint Authentication Method` to `None`, create a new Application of type `Single Page Application` or see the advice in [issues/93](https://github.com/auth0/auth0-react/issues/93#issuecomment-673431605))
+
+## 3. How to handle authentication in Electron or other native applications
+
+When using Auth0 React in Electron or other native/desktop applications, handling authentication redirects can be challenging because:
+
+1. The default redirect-based authentication flow requires page reloads, which may need special handling in native contexts
+2. Native apps often use custom URL schemes (like `electron://`, `file://`) for deep linking
+
+The `setAuthCallbackUrl` method provides a solution for these scenarios by allowing you to manually set the authentication callback URL with auth parameters.
+
+**Example usage in Electron:**
+
+```tsx
+import { useAuth0 } from '@auth0/auth0-react';
+
+function App() {
+ const { loginWithRedirect, setAuthCallbackUrl } = useAuth0();
+
+ // Set up a custom protocol handler in your Electron app
+ // When your app receives a deep link with auth params, call:
+ const handleAuthCallback = (url) => {
+ setAuthCallbackUrl(url);
+ };
+
+ return ;
+}
+```
+
+You would need to configure your Electron app to:
+
+1. Register a custom URL protocol (e.g., `your-app://`)
+2. Set your Auth0 application's callback URL to include this protocol in the **Allowed Callback URLs** section of the **Settings** tab of your Auth0 application.
+3. Intercept the callback URL in your main process and pass it to your renderer where you can call `setAuthCallbackUrl`
+
+This approach prevents having to reload the entire application when handling authentication callbacks and works well with custom URL schemes that native applications typically use.
diff --git a/__tests__/auth-provider.test.tsx b/__tests__/auth-provider.test.tsx
index 0ec66d3c..ffd3f4f4 100644
--- a/__tests__/auth-provider.test.tsx
+++ b/__tests__/auth-provider.test.tsx
@@ -3,7 +3,13 @@ import {
GetTokenSilentlyVerboseResponse,
} from '@auth0/auth0-spa-js';
import '@testing-library/jest-dom';
-import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
+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';
@@ -22,10 +28,7 @@ describe('Auth0Provider', () => {
it('should provide the Auth0Provider result', async () => {
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current).toBeDefined();
});
@@ -61,7 +64,9 @@ describe('Auth0Provider', () => {
});
it('should support redirectUri', async () => {
- const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
+ const warn = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => undefined);
const opts = {
clientId: 'foo',
domain: 'bar',
@@ -86,7 +91,9 @@ describe('Auth0Provider', () => {
});
it('should support authorizationParams.redirectUri', async () => {
- const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
+ const warn = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => undefined);
const opts = {
clientId: 'foo',
domain: 'bar',
@@ -135,10 +142,7 @@ describe('Auth0Provider', () => {
it('should check session when logged out', async () => {
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
@@ -151,10 +155,7 @@ describe('Auth0Provider', () => {
const user = { name: '__test_user__' };
clientMock.getUser.mockResolvedValue(user);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(clientMock.checkSession).toHaveBeenCalled();
expect(result.current.isAuthenticated).toBe(true);
@@ -168,10 +169,7 @@ describe('Auth0Provider', () => {
error_description: '__test_error_description__',
});
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(clientMock.checkSession).toHaveBeenCalled();
expect(() => {
@@ -235,10 +233,7 @@ describe('Auth0Provider', () => {
new Error('__test_error__')
);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(clientMock.handleRedirectCallback).toHaveBeenCalled();
expect(() => {
@@ -283,10 +278,7 @@ describe('Auth0Provider', () => {
const wrapper = createWrapper({
skipRedirectCallback: true,
});
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(clientMock.handleRedirectCallback).not.toHaveBeenCalled();
expect(result.current.isAuthenticated).toBe(true);
@@ -297,10 +289,7 @@ describe('Auth0Provider', () => {
it('should login with a popup', async () => {
clientMock.getUser.mockResolvedValue(undefined);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.user).toBeUndefined();
});
@@ -321,10 +310,7 @@ describe('Auth0Provider', () => {
it('should handle errors when logging in with a popup', async () => {
clientMock.getUser.mockResolvedValue(undefined);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBeUndefined();
@@ -348,12 +334,8 @@ describe('Auth0Provider', () => {
it('should provide a login method', async () => {
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
-
expect(result.current.loginWithRedirect).toBeInstanceOf(Function);
});
await result.current.loginWithRedirect({
@@ -369,12 +351,11 @@ describe('Auth0Provider', () => {
});
it('should provide a login method supporting redirectUri', async () => {
- const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
+ const warn = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => undefined);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.loginWithRedirect).toBeInstanceOf(Function);
});
@@ -390,12 +371,11 @@ describe('Auth0Provider', () => {
});
it('should provide a login method supporting authorizationParams.redirectUri', async () => {
- const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
+ const warn = jest
+ .spyOn(console, 'warn')
+ .mockImplementation(() => undefined);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.loginWithRedirect).toBeInstanceOf(Function);
});
@@ -416,10 +396,7 @@ describe('Auth0Provider', () => {
const user = { name: '__test_user__' };
clientMock.getUser.mockResolvedValue(user);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.logout).toBeInstanceOf(Function);
});
@@ -438,16 +415,13 @@ describe('Auth0Provider', () => {
// get logout to return a Promise to simulate async cache.
clientMock.logout.mockResolvedValue();
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true);
});
await act(async () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
- await result.current.logout({ openUrl: async () => { } });
+ await result.current.logout({ openUrl: async () => {} });
});
expect(result.current.isAuthenticated).toBe(false);
});
@@ -462,10 +436,7 @@ describe('Auth0Provider', () => {
logoutSpy();
});
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true);
});
@@ -479,10 +450,7 @@ describe('Auth0Provider', () => {
const user = { name: '__test_user__' };
clientMock.getUser.mockResolvedValue(user);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toBe(user);
@@ -502,10 +470,7 @@ describe('Auth0Provider', () => {
it('should provide a getAccessTokenSilently method', async () => {
clientMock.getTokenSilently.mockResolvedValue('token');
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.getAccessTokenSilently).toBeInstanceOf(Function);
});
@@ -525,10 +490,7 @@ describe('Auth0Provider', () => {
};
(clientMock.getTokenSilently as jest.Mock).mockResolvedValue(tokenResponse);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
const token = await result.current.getAccessTokenSilently({
detailedResponse: true,
@@ -541,10 +503,7 @@ describe('Auth0Provider', () => {
it('should normalize errors from getAccessTokenSilently method', async () => {
clientMock.getTokenSilently.mockRejectedValue(new ProgressEvent('error'));
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
await expect(result.current.getAccessTokenSilently).rejects.toThrowError(
'Get access token failed'
@@ -555,10 +514,7 @@ describe('Auth0Provider', () => {
it('should call getAccessTokenSilently in the scope of the Auth0 client', async () => {
clientMock.getTokenSilently.mockReturnThis();
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
const returnedThis = await result.current.getAccessTokenSilently();
@@ -570,10 +526,7 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(async () => {
expect(result.current.user?.name).toEqual('foo');
});
@@ -588,10 +541,7 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBeTruthy();
});
@@ -611,10 +561,7 @@ describe('Auth0Provider', () => {
const userObject = { name: 'foo' };
clientMock.getUser.mockResolvedValue(userObject);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(async () => {
const prevUser = result.current.user;
clientMock.getUser.mockResolvedValue(userObject);
@@ -629,11 +576,8 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
- let memoized
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
+ let memoized;
await waitFor(async () => {
memoized = result.current.getAccessTokenSilently;
expect(result.current.user?.name).toEqual('foo');
@@ -662,10 +606,7 @@ describe('Auth0Provider', () => {
it('should provide a getAccessTokenWithPopup method', async () => {
clientMock.getTokenWithPopup.mockResolvedValue('token');
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
expect(result.current.getAccessTokenWithPopup).toBeInstanceOf(Function);
await act(async () => {
const token = await result.current.getAccessTokenWithPopup();
@@ -677,10 +618,7 @@ describe('Auth0Provider', () => {
it('should call getAccessTokenWithPopup in the scope of the Auth0 client', async () => {
clientMock.getTokenWithPopup.mockReturnThis();
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
const returnedThis = await result.current.getAccessTokenWithPopup();
expect(returnedThis).toStrictEqual(clientMock);
@@ -691,10 +629,7 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
const prevUser = result.current.user;
clientMock.getUser.mockResolvedValue({ name: 'foo' });
@@ -708,10 +643,7 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(() => {
expect(result.current.isAuthenticated).toBeTruthy();
});
@@ -733,10 +665,7 @@ describe('Auth0Provider', () => {
const userObject = { name: 'foo' };
clientMock.getUser.mockResolvedValue(userObject);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(async () => {
const prevState = result.current;
clientMock.getUser.mockResolvedValue(userObject);
@@ -750,10 +679,7 @@ describe('Auth0Provider', () => {
it('should normalize errors from getAccessTokenWithPopup method', async () => {
clientMock.getTokenWithPopup.mockRejectedValue(new ProgressEvent('error'));
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
await expect(result.current.getAccessTokenWithPopup).rejects.toThrowError(
'Get access token failed'
@@ -780,16 +706,13 @@ describe('Auth0Provider', () => {
__raw: '__test_raw_token__',
});
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
expect(result.current.getIdTokenClaims).toBeInstanceOf(Function);
let claims;
await act(async () => {
claims = await result.current.getIdTokenClaims();
- })
+ });
expect(clientMock.getIdTokenClaims).toHaveBeenCalled();
expect(claims).toStrictEqual({
claim: '__test_claim__',
@@ -799,10 +722,9 @@ describe('Auth0Provider', () => {
it('should memoize the getIdTokenClaims method', async () => {
const wrapper = createWrapper();
- const { result, rerender } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result, rerender } = renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
await waitFor(() => {
const memoized = result.current.getIdTokenClaims;
rerender();
@@ -815,10 +737,7 @@ describe('Auth0Provider', () => {
appState: { redirectUri: '/' },
});
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
expect(result.current.handleRedirectCallback).toBeInstanceOf(Function);
await act(async () => {
const response = await result.current.handleRedirectCallback();
@@ -834,10 +753,7 @@ describe('Auth0Provider', () => {
it('should call handleRedirectCallback in the scope of the Auth0 client', async () => {
clientMock.handleRedirectCallback.mockReturnThis();
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
const returnedThis = await result.current.handleRedirectCallback();
expect(returnedThis).toStrictEqual(clientMock);
@@ -848,10 +764,7 @@ describe('Auth0Provider', () => {
clientMock.handleRedirectCallback.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
const prevUser = result.current.user;
clientMock.getUser.mockResolvedValue({ name: 'foo' });
@@ -865,10 +778,7 @@ describe('Auth0Provider', () => {
clientMock.handleRedirectCallback.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(async () => {
expect(result.current.isAuthenticated).toBeTruthy();
});
@@ -889,10 +799,7 @@ describe('Auth0Provider', () => {
const userObject = { name: 'foo' };
clientMock.getUser.mockResolvedValue(userObject);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await waitFor(async () => {
const prevState = result.current;
clientMock.getUser.mockResolvedValue(userObject);
@@ -901,7 +808,6 @@ describe('Auth0Provider', () => {
});
expect(result.current).toBe(prevState);
});
-
});
it('should normalize errors from handleRedirectCallback method', async () => {
@@ -909,10 +815,7 @@ describe('Auth0Provider', () => {
new ProgressEvent('error')
);
const wrapper = createWrapper();
- const { result } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
await act(async () => {
await expect(result.current.handleRedirectCallback).rejects.toThrowError(
'Get access token failed'
@@ -944,10 +847,9 @@ describe('Auth0Provider', () => {
clientMock.getTokenSilently.mockReturnThis();
clientMock.getUser.mockResolvedValue({ name: 'foo' });
const wrapper = createWrapper();
- const { result, rerender } = renderHook(
- () => useContext(Auth0Context),
- { wrapper }
- );
+ const { result, rerender } = renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
await waitFor(() => {
const memoized = result.current;
rerender();
@@ -1037,4 +939,124 @@ describe('Auth0Provider', () => {
expect(screen.queryByText('__custom_user__')).toBeInTheDocument();
expect(screen.queryByText('__main_user__')).not.toBeInTheDocument();
});
+
+ it('should provide a setAuthCallbackUrl method', async () => {
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useContext(Auth0Context), { wrapper });
+
+ await waitFor(() => {
+ expect(result.current.setAuthCallbackUrl).toBeInstanceOf(Function);
+ });
+ });
+
+ it.each([
+ 'https://example.com/callback?code=test_code&state=test_state',
+ 'http://localhost:3000/callback?code=test_code&state=test_state',
+ 'file://Application/dist/index.html?code=test_code&state=test_state',
+ 'chrome-extension://__test_extension_id__/callback?code=test_code&state=test_state',
+ 'electron-fidde://__test_fidder_id__/callback?code=test_code&state=test_state',
+ ])(
+ 'should handle authentication from setAuthCallbackUrl with query parameters',
+ async (testUrl) => {
+ const user = { name: '__test_user__' };
+ clientMock.getUser.mockResolvedValue(user);
+ clientMock.handleRedirectCallback.mockResolvedValue({
+ appState: { foo: 'bar' },
+ });
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ act(() => {
+ result.current.setAuthCallbackUrl(testUrl);
+ });
+
+ await waitFor(() => {
+ // The handleRedirectCallback should be called with the URL
+ expect(clientMock.handleRedirectCallback).toHaveBeenCalledWith(testUrl);
+ expect(result.current.user).toBe(user);
+ });
+ }
+ );
+
+ it.each([
+ 'https://example.com/callback',
+ 'https://example.com/callback?code=test_code',
+ 'https://example.com/callback?state=test_state',
+ ])(
+ 'should not process URLs without valid auth query parameters in setAuthCallbackUrl',
+ async (testUrl) => {
+ // Mock implementation to track calls
+ const handleCallbackSpy = jest.fn();
+ clientMock.handleRedirectCallback.mockImplementation(handleCallbackSpy);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ // Reset any previous calls to the mock
+ handleCallbackSpy.mockClear();
+
+ act(() => {
+ result.current.setAuthCallbackUrl(testUrl);
+ });
+
+ // Wait a bit to ensure the effect has time to run
+ await waitFor(() => {
+ expect(handleCallbackSpy).not.toHaveBeenCalled();
+ });
+ }
+ );
+
+ it('should call handleRedirectCallback once and reset authCallbackUrl after processing', async () => {
+ // Create a URL with auth parameters
+ const testUrl =
+ 'https://example.com/callback?code=test_code&state=test_state';
+
+ // Reset mocks
+ clientMock.handleRedirectCallback.mockClear();
+
+ // Create a wrapper with the Auth0Provider
+ const wrapper = createWrapper();
+ const { result, rerender } = renderHook(() => useContext(Auth0Context), {
+ wrapper,
+ });
+
+ // Wait for initial loading to complete
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ // Call setAuthCallbackUrl
+ act(() => {
+ result.current.setAuthCallbackUrl(testUrl);
+ });
+
+ // Verify handleRedirectCallback was called exactly once
+ await waitFor(() => {
+ expect(clientMock.handleRedirectCallback).toHaveBeenCalledTimes(1);
+ expect(clientMock.handleRedirectCallback).toHaveBeenCalledWith(testUrl);
+ });
+
+ // Clear the mock to check if it gets called on rerender
+ clientMock.handleRedirectCallback.mockClear();
+
+ // Force a rerender of the component
+ rerender();
+
+ await waitFor(() => {
+ expect(clientMock.handleRedirectCallback).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/__tests__/utils.test.tsx b/__tests__/utils.test.tsx
index 033769ca..50912c0b 100644
--- a/__tests__/utils.test.tsx
+++ b/__tests__/utils.test.tsx
@@ -1,5 +1,5 @@
-import { hasAuthParams, loginError, tokenError } from '../src/utils';
import { OAuthError } from '../src/errors';
+import { hasAuthParams, loginError, tokenError } from '../src/utils';
describe('utils hasAuthParams', () => {
it('should not recognise only the code param', async () => {
diff --git a/src/auth0-context.tsx b/src/auth0-context.tsx
index d2960ab9..d3aa31b6 100644
--- a/src/auth0-context.tsx
+++ b/src/auth0-context.tsx
@@ -1,14 +1,14 @@
import {
GetTokenSilentlyOptions,
+ GetTokenSilentlyVerboseResponse,
GetTokenWithPopupOptions,
IdToken,
- LogoutOptions as SPALogoutOptions,
- PopupLoginOptions,
PopupConfigOptions,
+ PopupLoginOptions,
RedirectLoginResult,
- User,
- GetTokenSilentlyVerboseResponse,
+ LogoutOptions as SPALogoutOptions,
RedirectLoginOptions as SPARedirectLoginOptions,
+ User,
} from '@auth0/auth0-spa-js';
import { createContext } from 'react';
import { AuthState, initialAuthState } from './auth-state';
@@ -38,7 +38,7 @@ export interface Auth0ContextInterface
*
* If refresh tokens are used, the token endpoint is called directly with the
* 'refresh_token' grant. If no refresh token is available to make this call,
- * the SDK will only fall back to using an iframe to the '/authorize' URL if
+ * the SDK will only fall back to using an iframe to the '/authorize' URL if
* the `useRefreshTokensFallback` setting has been set to `true`. By default this
* setting is `false`.
*
@@ -87,6 +87,21 @@ export interface Auth0ContextInterface
*/
getIdTokenClaims: () => Promise;
+ /**
+ * ```js
+ * setAuthCallbackUrl(url);
+ * ```
+ *
+ * Manually set the auth callback URL and trigger the authentication state handling.
+ *
+ * This is useful if you do not want to reload the page with authentication search query parameters
+ * in order to handle the authentication state. It can be particularly useful for native applications
+ * (like Electron), where reloading the page requires extra steps to be taken in the main process.
+ *
+ * @param url The URL containing the authentication result parameters to process
+ */
+ setAuthCallbackUrl: (url: string) => void;
+
/**
* ```js
* await loginWithRedirect(options);
@@ -159,6 +174,7 @@ export const initialContext = {
getAccessTokenSilently: stub,
getAccessTokenWithPopup: stub,
getIdTokenClaims: stub,
+ setAuthCallbackUrl: stub,
loginWithRedirect: stub,
loginWithPopup: stub,
logout: stub,
diff --git a/src/auth0-provider.tsx b/src/auth0-provider.tsx
index 9e4e09a2..1ce6afdd 100644
--- a/src/auth0-provider.tsx
+++ b/src/auth0-provider.tsx
@@ -1,3 +1,13 @@
+import {
+ Auth0Client,
+ Auth0ClientOptions,
+ GetTokenSilentlyOptions,
+ GetTokenWithPopupOptions,
+ PopupConfigOptions,
+ PopupLoginOptions,
+ RedirectLoginResult,
+ User,
+} from '@auth0/auth0-spa-js';
import React, {
useCallback,
useEffect,
@@ -6,29 +16,19 @@ import React, {
useRef,
useState,
} from 'react';
-import {
- Auth0Client,
- Auth0ClientOptions,
- PopupLoginOptions,
- PopupConfigOptions,
- GetTokenWithPopupOptions,
- RedirectLoginResult,
- GetTokenSilentlyOptions,
- User,
-} from '@auth0/auth0-spa-js';
+import { initialAuthState } from './auth-state';
import Auth0Context, {
Auth0ContextInterface,
LogoutOptions,
RedirectLoginOptions,
} from './auth0-context';
+import { reducer } from './reducer';
import {
+ deprecateRedirectUri,
hasAuthParams,
loginError,
tokenError,
- deprecateRedirectUri,
} from './utils';
-import { reducer } from './reducer';
-import { initialAuthState } from './auth-state';
/**
* The state of the application before the user was redirected to the login page.
@@ -144,6 +144,7 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
() => new Auth0Client(toAuth0ClientOptions(clientOpts))
);
const [state, dispatch] = useReducer(reducer, initialAuthState);
+ const [authCallbackUrl, setAuthCallbackUrl] = useState();
const didInitialise = useRef(false);
const handleError = useCallback((error: Error) => {
@@ -151,16 +152,15 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
return error;
}, []);
- useEffect(() => {
- if (didInitialise.current) {
- return;
- }
- didInitialise.current = true;
- (async (): Promise => {
+ const handleAuthCallback = useCallback(
+ async (authParams?: string, authCallbackUrl?: string) => {
try {
let user: User | undefined;
- if (hasAuthParams() && !skipRedirectCallback) {
- const { appState } = await client.handleRedirectCallback();
+
+ if (hasAuthParams(authParams) && !skipRedirectCallback) {
+ const { appState } = await client.handleRedirectCallback(
+ authCallbackUrl
+ );
user = await client.getUser();
onRedirectCallback(appState, user);
} else {
@@ -171,8 +171,36 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
} catch (error) {
handleError(loginError(error));
}
- })();
- }, [client, onRedirectCallback, skipRedirectCallback, handleError]);
+ },
+ [client, onRedirectCallback, skipRedirectCallback, handleError]
+ );
+
+ useEffect(() => {
+ if (didInitialise.current) {
+ return;
+ }
+ didInitialise.current = true;
+ handleAuthCallback();
+ }, [
+ client,
+ onRedirectCallback,
+ skipRedirectCallback,
+ handleError,
+ handleAuthCallback,
+ ]);
+
+ useEffect(() => {
+ if (authCallbackUrl === undefined) {
+ return;
+ }
+
+ setAuthCallbackUrl(undefined);
+ const idx = authCallbackUrl.indexOf('?');
+
+ if (idx !== -1) {
+ handleAuthCallback(authCallbackUrl.slice(idx), authCallbackUrl);
+ }
+ }, [authCallbackUrl, handleAuthCallback]);
const loginWithRedirect = useCallback(
(opts?: RedirectLoginOptions): Promise => {
@@ -198,6 +226,7 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
const user = await client.getUser();
dispatch({ type: 'LOGIN_POPUP_COMPLETE', user });
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[client]
);
@@ -278,6 +307,7 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
getAccessTokenSilently,
getAccessTokenWithPopup,
getIdTokenClaims,
+ setAuthCallbackUrl,
loginWithRedirect,
loginWithPopup,
logout,
@@ -288,6 +318,7 @@ const Auth0Provider = (opts: Auth0ProviderOptions) => {
getAccessTokenSilently,
getAccessTokenWithPopup,
getIdTokenClaims,
+ setAuthCallbackUrl,
loginWithRedirect,
loginWithPopup,
logout,
diff --git a/src/use-auth0.tsx b/src/use-auth0.tsx
index 8bc03eae..84d9df32 100644
--- a/src/use-auth0.tsx
+++ b/src/use-auth0.tsx
@@ -1,5 +1,5 @@
-import { useContext } from 'react';
import { User } from '@auth0/auth0-spa-js';
+import { useContext } from 'react';
import Auth0Context, { Auth0ContextInterface } from './auth0-context';
/**
@@ -14,6 +14,7 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context';
* getAccessTokenSilently,
* getAccessTokenWithPopup,
* getIdTokenClaims,
+ * setAuthCallbackUrl,
* loginWithRedirect,
* loginWithPopup,
* logout,