Upgrade MSAL library version (#2454)

* Reapply "MSAL browser migration changes"

This reverts commit 60a65efb7b.

* Fix redirect URI for localhost

* Fix URI for logout and other minor fix

* Remove unnecessary files

* Fix tests

* Fix tests

* Run npm format

* Address comments

* Address comment
This commit is contained in:
sindhuba
2026-04-08 10:40:06 -07:00
committed by GitHub
parent fb250259ed
commit 339ba4f295
12 changed files with 103 additions and 49 deletions

Binary file not shown.

16
package-lock.json generated
View File

@@ -13,7 +13,7 @@
"@azure/cosmos": "4.7.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
"@azure/msal-browser": "^5.2.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.119.0",
@@ -590,21 +590,22 @@
"license": "0BSD"
},
"node_modules/@azure/msal-browser": {
"version": "2.14.2",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.6.0.tgz",
"integrity": "sha512-LLqyAtpQNfnATQKnplg/dKJaigxGaaMPrp003ZWGnWwsAmmtzk7xcHEVykCu/4FMyyIfn66NPPzxS9DHrg/UOA==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "^4.3.0"
"@azure/msal-common": "16.4.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "4.5.1",
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.4.0.tgz",
"integrity": "sha512-twXt09PYtj1PffNNIAzQlrBd0DS91cdA6i1gAfzJ6BnPM4xNk5k9q/5xna7jLIjU3Jnp0slKYtucshGM8OGNAw==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1"
},
"engines": {
"node": ">=0.8.0"
}
@@ -15860,6 +15861,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [

View File

@@ -8,7 +8,7 @@
"@azure/cosmos": "4.7.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
"@azure/msal-browser": "^5.2.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.119.0",

View File

@@ -280,7 +280,7 @@ export default class Explorer {
updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (error) {
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorCodes.popupWindowError) {
logConsoleError(
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again",
);

View File

@@ -1,7 +1,4 @@
import {
AuthError as msalAuthError,
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
} from "@azure/msal-browser";
import { AuthError as msalAuthError, BrowserAuthErrorCodes as msalBrowserAuthErrorCodes } from "@azure/msal-browser";
import {
Checkbox,
ChoiceGroup,
@@ -305,7 +302,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
} catch (authError) {
if (
authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
authError.errorCode === msalBrowserAuthErrorCodes.popupWindowError
) {
logConsoleError(t(Keys.panes.settings.popupsDisabledError));
} else {

View File

@@ -1,12 +1,14 @@
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { resetConfigContext } from "../ConfigContext";
import { ApiType, updateUserContext, userContext } from "../UserContext";
import * as AuthorizationUtils from "./AuthorizationUtils";
jest.mock("../Explorer/Explorer");
jest.mock("@azure/msal-browser", () => ({
PublicClientApplication: jest.fn().mockImplementation((config) => ({
_config: config,
initialize: jest.fn().mockResolvedValue(undefined),
handleRedirectPromise: jest.fn().mockResolvedValue(null),
})),
}));
@@ -138,41 +140,27 @@ describe("AuthorizationUtils", () => {
});
describe("getMsalInstance()", () => {
const originalHostname = window.location.hostname;
const originalNodeEnv = process.env.NODE_ENV;
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
resetConfigContext();
Object.defineProperty(window, "location", {
value: { ...window.location, hostname: originalHostname },
writable: true,
});
});
it("should use configContext.msalRedirectURI when set", async () => {
updateConfigContext({ msalRedirectURI: "https://dataexplorer-preview.azurewebsites.net/" });
it("should use dev redirect bridge URL in development mode", async () => {
process.env.NODE_ENV = "development";
const instance = await AuthorizationUtils.getMsalInstance();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((instance as any)._config.auth.redirectUri).toBe("https://dataexplorer-preview.azurewebsites.net/");
expect((instance as any)._config.auth.redirectUri).toBe(
"https://dataexplorer-dev.azurewebsites.net/redirectBridge.html",
);
});
it("should use dev redirect URI on localhost", async () => {
Object.defineProperty(window, "location", {
value: { ...window.location, hostname: "localhost" },
writable: true,
});
it("should use origin-based redirect bridge URL in production", async () => {
process.env.NODE_ENV = "production";
const instance = await AuthorizationUtils.getMsalInstance();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((instance as any)._config.auth.redirectUri).toBe("https://dataexplorer-dev.azurewebsites.net");
});
it("should not set redirect URI in non-localhost production", async () => {
Object.defineProperty(window, "location", {
value: { ...window.location, hostname: "cosmos.azure.com" },
writable: true,
});
const instance = await AuthorizationUtils.getMsalInstance();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((instance as any)._config.auth.redirectUri).toBeUndefined();
expect((instance as any)._config.auth.redirectUri).toBe("http://localhost/redirectBridge.html");
});
});
});

View File

@@ -51,6 +51,12 @@ export function decryptJWTToken(token: string) {
}
export async function getMsalInstance() {
// Compute the redirect bridge URL for MSAL v5 COOP handling
const redirectBridgeUrl =
process.env.NODE_ENV === "development"
? "https://dataexplorer-dev.azurewebsites.net/redirectBridge.html"
: `${window.location.origin}/redirectBridge.html`;
const msalConfig: msal.Configuration = {
cache: {
cacheLocation: "localStorage",
@@ -58,16 +64,16 @@ export async function getMsalInstance() {
auth: {
authority: `${configContext.AAD_ENDPOINT}organizations`,
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
// MSAL v5 requires redirect bridge for popup/silent flows
redirectUri: redirectBridgeUrl,
},
};
if (configContext.msalRedirectURI) {
msalConfig.auth.redirectUri = configContext.msalRedirectURI;
} else if (process.env.NODE_ENV === "development" || window.location.hostname === "localhost") {
msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
}
const msalInstance = new msal.PublicClientApplication(msalConfig);
// v3+ requires explicit initialization before using MSAL APIs
await msalInstance.initialize();
// Handle any redirect response (e.g., after logoutRedirect) to clear interaction state
await msalInstance.handleRedirectPromise();
return msalInstance;
}

View File

@@ -58,10 +58,15 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
if (!msalInstance || !config) {
return;
}
// Use redirect bridge for MSAL v5 COOP handling
const redirectBridgeUrl =
process.env.NODE_ENV === "development"
? "https://dataexplorer-dev.azurewebsites.net/redirectBridge.html"
: `${window.location.origin}/redirectBridge.html`;
try {
const response = await msalInstance.loginPopup({
redirectUri: config.msalRedirectURI,
redirectUri: redirectBridgeUrl,
scopes: [],
});
setLoggedIn();
@@ -81,7 +86,12 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
}
setLoggedOut();
localStorage.removeItem("cachedTenantId");
msalInstance.logoutRedirect();
// Redirect back to the hosted explorer after logout
const postLogoutRedirectUri =
process.env.NODE_ENV === "development"
? "https://dataexplorer-dev.azurewebsites.net/hostedExplorer.html"
: `${window.location.origin}`;
msalInstance.logoutRedirect({ postLogoutRedirectUri });
}, [msalInstance]);
const switchTenant = React.useCallback(
@@ -89,9 +99,14 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
if (!msalInstance || !config) {
return;
}
// Use redirect bridge for MSAL v5 COOP handling
const redirectBridgeUrl =
process.env.NODE_ENV === "development"
? "https://dataexplorer-dev.azurewebsites.net/redirectBridge.html"
: `${window.location.origin}/redirectBridge.html`;
try {
const response = await msalInstance.loginPopup({
redirectUri: config.msalRedirectURI,
redirectUri: redirectBridgeUrl,
authority: `${config.AAD_ENDPOINT}${id}`,
scopes: [],
});
@@ -120,7 +135,7 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
setArmToken(armToken);
setAuthFailure(null);
} catch (error) {
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorCodes.popupWindowError) {
// This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively
// and user has popups disabled in browser. This fails as the popup is not the result of a explicit user
// action. In this case, we display the failure and a link to repeat the operation. Clicking on the

14
src/redirectBridge.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authentication Redirect</title>
</head>
<body>
<!-- MSAL COOP Redirect Bridge - handles auth response from Identity Provider -->
<div id="redirect-container">
<p>Processing authentication...</p>
</div>
</body>
</html>

15
src/redirectBridge.ts Normal file
View File

@@ -0,0 +1,15 @@
/**
* MSAL COOP Redirect Bridge
*
* This page handles the authentication response from the Identity Provider (IdP)
* and broadcasts it to the main application frame. Required for msal-browser v5+
* to securely handle auth responses when the IdP sets Cross-Origin-Opener-Policy headers.
*
* Security Note: This file must be bundled with your application, NOT loaded from a CDN.
*
*/
import { broadcastResponseToMainFrame } from "@azure/msal-browser/redirect-bridge";
broadcastResponseToMainFrame().catch((error: unknown) => {
console.error("MSAL redirect bridge error:", error);
});

View File

@@ -0,0 +1,11 @@
// Type declarations for @azure/msal-browser subpath exports
// Required because tsconfig uses moduleResolution: "node" which doesn't support exports field
declare module "@azure/msal-browser/redirect-bridge" {
/**
* Processes the authentication response from the redirect URL.
* For SSO and popup scenarios broadcasts it to the main frame.
* For redirect scenario navigates to the home page.
*/
export function broadcastResponseToMainFrame(navigationClient?: unknown): Promise<void>;
}

View File

@@ -116,6 +116,7 @@ module.exports = function (_env = {}, argv = {}) {
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
selfServe: "./src/SelfServe/SelfServe.tsx",
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
redirectBridge: "./src/redirectBridge.ts",
...(mode !== "production" && { testExplorer: "./test/testExplorer/TestExplorer.ts" }),
...(mode !== "production" && {
searchableDropdownFixture: "./test/component-fixtures/searchableDropdown/SearchableDropdownFixture.tsx",
@@ -168,6 +169,11 @@ module.exports = function (_env = {}, argv = {}) {
template: "src/SelfServe/selfServe.html",
chunks: ["selfServe"],
}),
new HtmlWebpackPlugin({
filename: "redirectBridge.html",
template: "src/redirectBridge.html",
chunks: ["redirectBridge"],
}),
...(mode !== "production"
? [
new HtmlWebpackPlugin({