From 275d84556137070d1acfbd3758e7591c44c3c61c Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Thu, 8 Dec 2022 12:56:58 +0100 Subject: [PATCH] Add openUrl and deprecate onRedirect (#1058) * Add openUrl and deprecate onRedirect * Update MIGRATION_GUIDE to refer to openUrl instead of onRedirect * Use openUrl: false when catching login_required * Remove boolean from openUrl in loginWithRedirect * revert changes --- MIGRATION_GUIDE.md | 20 +++--- __tests__/Auth0Client/helpers.ts | 5 +- .../Auth0Client/loginWithRedirect.test.ts | 42 ++++++++++- __tests__/Auth0Client/logout.test.ts | 69 ++++++++++++++++++- src/Auth0Client.ts | 20 +++--- src/Auth0Client.utils.ts | 23 ++++++- src/global.ts | 28 ++++++++ 7 files changed, 182 insertions(+), 25 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b75b52562..0d1740bf4 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -11,7 +11,7 @@ With the v2 release of Auth0-SPA-JS, we have improved both performance and devel - [Introduction of logoutParams](#introduction-of-logoutparams) - [buildAuthorizeUrl has been removed](#buildauthorizeurl-has-been-removed) - [buildLogoutUrl has been removed](#buildlogouturl-has-been-removed) - - [localOnly logout has been removed, and replaced by onRedirect](#localonly-logout-has-been-removed-and-replaced-by-onredirect) + - [localOnly logout has been removed, and replaced by openUrl](#localonly-logout-has-been-removed-and-replaced-by-openurl) - [redirectMethod has been removed from loginWithRedirect](#redirectmethod-has-been-removed-from-loginwithredirect) - [ignoreCache on getTokenSilentlyhas been replaced by cacheMode](#ignorecache-on-gettokensilently-has-been-replaced-by-cachemode) - [application/x-www-form-urlencoded is used by default instead of application/json](#applicationx-www-form-urlencoded-is-used-by-default-instead-of-applicationjson) @@ -148,11 +148,11 @@ const url = await client.buildAuthorizeUrl(); await Browser.open({ url }); ``` -With v2, we have removed `buildAuthorizeUrl`. This means that the snippet above will no longer work, and you should update your code by using `onRedirect` instead. +With v2, we have removed `buildAuthorizeUrl`. This means that the snippet above will no longer work, and you should update your code by using `openUrl` instead. ```ts await client.loginWithRedirect({ - async onRedirect(url) { + async openUrl(url) { // Redirect using Capacitor's Browser plugin await Browser.open({ url }); } @@ -172,11 +172,11 @@ const url = await client.buildLogoutUrl(); await Browser.open({ url }); ``` -With v2, `buildLogoutUrl` has been removed and you should update any code that is not able to rely on `window.location.assign` to use `onRedirect` when calling `logout`: +With v2, `buildLogoutUrl` has been removed and you should update any code that is not able to rely on `window.location.assign` to use `openUrl` when calling `logout`: ```ts client.logout({ - async onRedirect(url) { + async openUrl(url) { // Redirect using Capacitor's Browser plugin await Browser.open({ url }); } @@ -185,15 +185,15 @@ client.logout({ This method was removed because, when using our SDK, the logout method is expected to be called regardless of the browser used. Instead of calling both `logout` and `buildLogoutUrl`, you can now change the redirect behaviour when calling `logout`. -### `localOnly` logout has been removed, and replaced by `onRedirect` +### `localOnly` logout has been removed, and replaced by `openUrl: false` When calling the SDK's `logout` method, v1 supports the ability to specify `localOnly: true`, ensuring our SDK does not redirect to Auth0 but only clears the user state from the application. -With v2, we have removed `localOnly`, but instead provided a way for developers to take control of the redirect behavior by setting `onRedirect`. In order to achieve localOnly logout with v2, you should set `onRedirect` to a noop function. +With v2, we have removed `localOnly`, but instead provided a way for developers to take control of the redirect behavior by setting `openUrl`. In order to achieve localOnly logout with v2, you should set `openUrl` to false. ```ts client.logout({ - async onRedirect() {} + openUrl: false }); ``` @@ -207,11 +207,11 @@ await client.loginWithRedirect({ }); ``` -With the release of v2, we have removed `redirectMethod`. If you want to use anything but `window.location.assign` to handle the redirect to Auth0, you should implement `onRedirect`: +With the release of v2, we have removed `redirectMethod`. If you want to use anything but `window.location.assign` to handle the redirect to Auth0, you should implement `openUrl`: ```ts await client.loginWithRedirect({ - async onRedirect(url) { + async openUrl(url) { window.location.replace(url); } }); diff --git a/__tests__/Auth0Client/helpers.ts b/__tests__/Auth0Client/helpers.ts index eb997a1f4..794282c4b 100644 --- a/__tests__/Auth0Client/helpers.ts +++ b/__tests__/Auth0Client/helpers.ts @@ -22,6 +22,7 @@ import { TEST_STATE } from '../constants'; import { expect } from '@jest/globals'; +import { patchOpenUrlWithOnRedirect } from '../../src/Auth0Client.utils'; const authorizationResponse: AuthenticationResult = { code: 'my_code', @@ -194,7 +195,9 @@ export const loginWithRedirectFn = (mockWindow, mockFetch) => { } = processDefaultLoginWithRedirectOptions(testConfig); await auth0.loginWithRedirect(options); - if (!options?.onRedirect) { + const patchesOptions = options && patchOpenUrlWithOnRedirect(options); + + if (!patchesOptions || patchesOptions.openUrl == null) { expect(mockWindow.location.assign).toHaveBeenCalled(); } diff --git a/__tests__/Auth0Client/loginWithRedirect.test.ts b/__tests__/Auth0Client/loginWithRedirect.test.ts index 5eb1fd675..45363a968 100644 --- a/__tests__/Auth0Client/loginWithRedirect.test.ts +++ b/__tests__/Auth0Client/loginWithRedirect.test.ts @@ -279,6 +279,42 @@ describe('Auth0Client', () => { ); }); + it('should log the user in by calling window.location.replace when specifying it as openUrl', async () => { + const auth0 = setup(); + + await loginWithRedirect(auth0, { + authorizationParams: { + audience: 'test_audience' + }, + openUrl: async url => window.location.replace(url) + }); + + const url = new URL(mockWindow.location.replace.mock.calls[0][0]); + + assertUrlEquals( + url, + TEST_DOMAIN, + '/authorize', + { + audience: 'test_audience' + }, + false + ); + }); + + it('skips `window.location.assign` when `options.openUrl` is provided', async () => { + const auth0 = setup(); + + await loginWithRedirect(auth0, { + authorizationParams: { + audience: 'test_audience' + }, + openUrl: async () => {} + }); + + expect(window.location.assign).not.toHaveBeenCalled(); + }); + it('should log the user in with custom params', async () => { const auth0 = setup(); @@ -456,11 +492,13 @@ describe('Auth0Client', () => { await loginWithRedirect(auth0); expect(esCookie.remove).toHaveBeenCalledWith( - `auth0.${TEST_CLIENT_ID}.organization_hint`, {} + `auth0.${TEST_CLIENT_ID}.organization_hint`, + {} ); expect(esCookie.remove).toHaveBeenCalledWith( - `_legacy_auth0.${TEST_CLIENT_ID}.organization_hint`, {} + `_legacy_auth0.${TEST_CLIENT_ID}.organization_hint`, + {} ); }); diff --git a/__tests__/Auth0Client/logout.test.ts b/__tests__/Auth0Client/logout.test.ts index 06553aaa1..8d3016642 100644 --- a/__tests__/Auth0Client/logout.test.ts +++ b/__tests__/Auth0Client/logout.test.ts @@ -155,10 +155,21 @@ describe('Auth0Client', () => { ); }); - it('removes the organization hint cookie from storage when `options.onRedirect` is set', async () => { + it('removes authenticated cookie from storage when `options.openUrl` is set', async () => { const auth0 = setup(); - await auth0.logout({ onRedirect: async () => {} }); + await auth0.logout({ openUrl: async () => {} }); + + expect(esCookie.remove).toHaveBeenCalledWith( + `auth0.${TEST_CLIENT_ID}.is.authenticated`, + {} + ); + }); + + it('removes the organization hint cookie from storage when `options.openUrl` is set', async () => { + const auth0 = setup(); + + await auth0.logout({ openUrl: async () => {} }); expect(esCookie.remove).toHaveBeenCalledWith( `auth0.${TEST_CLIENT_ID}.organization_hint`, @@ -179,6 +190,19 @@ describe('Auth0Client', () => { ); }); + it('skips `window.location.assign` when `options.openUrl` is provided', async () => { + const auth0 = setup(); + const openUrl = jest.fn(); + await auth0.logout({ openUrl }); + + expect(window.location.assign).not.toHaveBeenCalled(); + expect(openUrl).toHaveBeenCalledWith( + expect.stringContaining( + 'https://auth0_domain/v2/logout?client_id=auth0_client_id' + ) + ); + }); + it('calls `window.location.assign` when `options.onRedirect` is not provided', async () => { const auth0 = setup(); @@ -186,6 +210,13 @@ describe('Auth0Client', () => { expect(window.location.assign).toHaveBeenCalled(); }); + it('calls `window.location.assign` when `options.openUrl` is not provided', async () => { + const auth0 = setup(); + + await auth0.logout(); + expect(window.location.assign).toHaveBeenCalled(); + }); + it('can access isAuthenticated immediately after local logout', async () => { const auth0 = setup(); @@ -196,6 +227,16 @@ describe('Auth0Client', () => { expect(await auth0.isAuthenticated()).toBe(false); }); + it('can access isAuthenticated immediately after local logout', async () => { + const auth0 = setup(); + + await loginWithRedirect(auth0); + expect(await auth0.isAuthenticated()).toBe(true); + await auth0.logout({ openUrl: async () => {} }); + + expect(await auth0.isAuthenticated()).toBe(false); + }); + it('can access isAuthenticated immediately after local logout when using a custom async cache', async () => { const auth0 = setup({ cache: new InMemoryAsyncCacheNoKeys() @@ -208,6 +249,18 @@ describe('Auth0Client', () => { expect(await auth0.isAuthenticated()).toBe(false); }); + it('can access isAuthenticated immediately after local logout when using a custom async cache - using openUrl', async () => { + const auth0 = setup({ + cache: new InMemoryAsyncCacheNoKeys() + }); + + await loginWithRedirect(auth0); + expect(await auth0.isAuthenticated()).toBe(true); + await auth0.logout({ openUrl: async () => {} }); + + expect(await auth0.isAuthenticated()).toBe(false); + }); + it('can access isAuthenticated immediately after local logout when using a custom async cache', async () => { const auth0 = setup({ cache: new InMemoryAsyncCacheNoKeys() @@ -220,6 +273,18 @@ describe('Auth0Client', () => { expect(await auth0.isAuthenticated()).toBe(false); }); + it('can access isAuthenticated immediately after local logout when using a custom async cache - using openUrl', async () => { + const auth0 = setup({ + cache: new InMemoryAsyncCacheNoKeys() + }); + + await loginWithRedirect(auth0); + expect(await auth0.isAuthenticated()).toBe(true); + await auth0.logout({ openUrl: async () => {} }); + + expect(await auth0.isAuthenticated()).toBe(false); + }); + it('correctly handles a null clientId value', async () => { const auth0 = setup(); await auth0.logout({ clientId: null }); diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts index d9fd72f0a..7389fc7a3 100644 --- a/src/Auth0Client.ts +++ b/src/Auth0Client.ts @@ -89,7 +89,8 @@ import { cacheFactory, getAuthorizeParams, GET_TOKEN_SILENTLY_LOCK_KEY, - OLD_IS_AUTHENTICATED_COOKIE_NAME + OLD_IS_AUTHENTICATED_COOKIE_NAME, + patchOpenUrlWithOnRedirect } from './Auth0Client.utils'; /** @@ -444,7 +445,8 @@ export class Auth0Client { public async loginWithRedirect( options: RedirectLoginOptions = {} ) { - const { onRedirect, fragment, appState, ...urlOptions } = options; + const { openUrl, fragment, appState, ...urlOptions } = + patchOpenUrlWithOnRedirect(options); const organizationId = urlOptions.authorizationParams?.organization || @@ -462,8 +464,8 @@ export class Auth0Client { const urlWithFragment = fragment ? `${url}#${fragment}` : url; - if (onRedirect) { - await onRedirect(urlWithFragment); + if (openUrl) { + await openUrl(urlWithFragment); } else { window.location.assign(urlWithFragment); } @@ -824,7 +826,7 @@ export class Auth0Client { * @param options */ public async logout(options: LogoutOptions = {}): Promise { - const { onRedirect, ...logoutOptions } = options; + const { openUrl, ...logoutOptions } = patchOpenUrlWithOnRedirect(options); await this.cacheManager.clear(); @@ -838,9 +840,9 @@ export class Auth0Client { const url = this._buildLogoutUrl(logoutOptions); - if (onRedirect) { - await onRedirect(url); - } else { + if (openUrl) { + await openUrl(url); + } else if (openUrl !== false) { window.location.assign(url); } } @@ -918,7 +920,7 @@ export class Auth0Client { } catch (e) { if (e.error === 'login_required') { this.logout({ - onRedirect: async () => {} + openUrl: false }); } throw e; diff --git a/src/Auth0Client.utils.ts b/src/Auth0Client.utils.ts index 0ea54dedc..f148cd72a 100644 --- a/src/Auth0Client.utils.ts +++ b/src/Auth0Client.utils.ts @@ -2,7 +2,8 @@ import { ICache, InMemoryCache, LocalStorageCache } from './cache'; import { Auth0ClientOptions, AuthorizationParams, - AuthorizeOptions + AuthorizeOptions, + LogoutOptions } from './global'; import { getUniqueScopes } from './scope'; @@ -73,3 +74,23 @@ export const getAuthorizeParams = ( code_challenge_method: 'S256' }; }; + +/** + * @ignore + * + * Function used to provide support for the deprecated onRedirect through openUrl. + */ +export const patchOpenUrlWithOnRedirect = < + T extends Pick +>( + options: T +) => { + const { openUrl, onRedirect, ...originalOptions } = options; + + const result = { + ...originalOptions, + openUrl: openUrl === false || openUrl ? openUrl : onRedirect + }; + + return result as T; +}; diff --git a/src/global.ts b/src/global.ts index f24f262fd..b48a39ca3 100644 --- a/src/global.ts +++ b/src/global.ts @@ -296,8 +296,21 @@ export interface RedirectLoginOptions * window.location.replace(url); * } * }); + * @deprecated since v2.0.1, use `openUrl` instead. */ onRedirect?: (url: string) => Promise; + + /** + * Used to control the redirect and not rely on the SDK to do the actual redirect. + * + * @example + * const client = new Auth0Client({ + * async openUrl(url) { + * window.location.replace(url); + * } + * }); + */ + openUrl?: (url: string) => Promise; } export interface RedirectLoginResult { @@ -442,8 +455,23 @@ export interface LogoutOptions extends LogoutUrlOptions { * window.location.replace(url); * } * }); + * @deprecated since v2.0.1, use `openUrl` instead. */ onRedirect?: (url: string) => Promise; + + /** + * Used to control the redirect and not rely on the SDK to do the actual redirect. + * + * Set to `false` to disable the redirect, or provide a function to handle the actual redirect yourself. + * + * @example + * await auth0.logout({ + * async openUrl(url) { + * window.location.replace(url); + * } + * }); + */ + openUrl?: false | ((url: string) => Promise); } /**