Skip to content

Commit

Permalink
Handle edge-case where OAuth redirect opens new tab instead (#3900)
Browse files Browse the repository at this point in the history
* Handle edge-case where OAuth redirect opens new tab instead

- Fixes #3899

* Minor simplification
  • Loading branch information
amcclain authored Jan 14, 2025
1 parent df5239f commit c3984ac
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 16 deletions.
29 changes: 29 additions & 0 deletions security/BaseOAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
*
* Copyright © 2025 Extremely Heavy Industries Inc.
*/
import {br, fragment} from '@xh/hoist/cmp/layout';
import {HoistBase, managed, XH} from '@xh/hoist/core';
import {Icon} from '@xh/hoist/icon';
import {action, makeObservable} from '@xh/hoist/mobx';
import {never, wait} from '@xh/hoist/promise';
import {Token, TokenMap} from '@xh/hoist/security/Token';
import {Timer} from '@xh/hoist/utils/async';
import {MINUTES, olderThan, ONE_MINUTE, SECONDS} from '@xh/hoist/utils/datetime';
Expand Down Expand Up @@ -285,6 +288,32 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
window.history.replaceState(null, '', pathname + search);
}

/** Call after requesting the provider library redirect the user away for auth. */
protected async maskAfterRedirect() {
// We expect the tab to unload momentarily and be redirected to the provider's login page.
// Wait here to ensure we mask the app while the browser processes the redirect...
await wait(10 * SECONDS);

// ...but also handle an observed edge case where the browser decided to open a new tab
// instead of redirecting the current one (https://github.com/xh/hoist-react/issues/3899).
// Show message below if/when user swaps back to the original tab, vs. endless spinner.
await XH.alert({
title: 'Auth / Redirect Error',
message: fragment(
'Authentication did not complete as expected / tab was not redirected.',
br(),
br(),
'Please click below or refresh this tab in your browser to try again.'
),
confirmProps: {text: 'Reload', icon: Icon.refresh(), intent: 'primary', minimal: false}
});

XH.reloadApp();

// Ensure stale init does not progress - this tab should *really* be on its way out now!
await never();
}

protected async fetchAllTokensAsync(useCache = true): Promise<TokenMap> {
const ret: TokenMap = {},
{accessSpecs} = this;
Expand Down
6 changes: 4 additions & 2 deletions security/authzero/AuthZeroClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type {Auth0ClientOptions} from '@auth0/auth0-spa-js';
import {Auth0Client} from '@auth0/auth0-spa-js';
import {XH} from '@xh/hoist/core';
import {never, wait} from '@xh/hoist/promise';
import {wait} from '@xh/hoist/promise';
import {Token, TokenMap} from '@xh/hoist/security/Token';
import {SECONDS} from '@xh/hoist/utils/datetime';
import {mergeDeep, throwIf} from '@xh/hoist/utils/js';
Expand Down Expand Up @@ -99,11 +99,13 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe

protected override async doLoginRedirectAsync(): Promise<void> {
const appState = this.captureRedirectState();

await this.client.loginWithRedirect({
appState,
authorizationParams: {scope: this.loginScope}
});
await never();

await this.maskAfterRedirect();
}

protected override async doLoginPopupAsync(): Promise<void> {
Expand Down
26 changes: 12 additions & 14 deletions security/msal/MsalClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import {
IPublicClientApplication,
LogLevel,
PopupRequest,
RedirectRequest,
SilentRequest
} from '@azure/msal-browser';
import {XH} from '@xh/hoist/core';
import {never} from '@xh/hoist/promise';
import {Token, TokenMap} from '@xh/hoist/security/Token';
import {logDebug, logError, logInfo, logWarn, mergeDeep, throwIf} from '@xh/hoist/utils/js';
import {flatMap, union, uniq} from 'lodash';
Expand Down Expand Up @@ -201,18 +199,18 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
}

protected override async doLoginRedirectAsync(): Promise<void> {
const {client} = this,
state = this.captureRedirectState(),
opts: RedirectRequest = {
state,
loginHint: this.getSelectedUsername(),
domainHint: this.config.domainHint,
scopes: this.loginScopes,
extraScopesToConsent: this.loginExtraScopesToConsent,
redirectUri: this.redirectUrl
};
await client.acquireTokenRedirect(opts);
await never();
const state = this.captureRedirectState();

await this.client.acquireTokenRedirect({
state,
loginHint: this.getSelectedUsername(),
domainHint: this.config.domainHint,
scopes: this.loginScopes,
extraScopesToConsent: this.loginExtraScopesToConsent,
redirectUri: this.redirectUrl
});

await this.maskAfterRedirect();
}

protected override async fetchIdTokenAsync(useCache: boolean = true): Promise<Token> {
Expand Down

0 comments on commit c3984ac

Please sign in to comment.