mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-16 19:39:19 +01:00
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:
BIN
.vs/slnx.sqlite
BIN
.vs/slnx.sqlite
Binary file not shown.
16
package-lock.json
generated
16
package-lock.json
generated
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
14
src/redirectBridge.html
Normal 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
15
src/redirectBridge.ts
Normal 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);
|
||||
});
|
||||
11
src/types/msal-browser-redirect-bridge.d.ts
vendored
Normal file
11
src/types/msal-browser-redirect-bridge.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user