Improve error handling when acquiring aad tokens (#1746)

* Mostly working - some cosmetic changes remaining.

* Cosmetic changes and other tidy ups.

* More clean up.

* Move msal back to dependencies. Fix typo.

* msal should be prod dependency

* Revert msal package update as it is causing issues with unit test
execution.

* Add tracing for unhandled exceptions when acquiring tokens.
This commit is contained in:
jawelton74
2024-03-04 16:08:13 -08:00
committed by GitHub
parent 932f211038
commit 76ad930930
8 changed files with 209 additions and 34 deletions

View File

@@ -2,9 +2,9 @@ import * as msal from "@azure/msal-browser";
import { useBoolean } from "@fluentui/react-hooks";
import * as React from "react";
import { configContext } from "../ConfigContext";
import { getMsalInstance } from "../Utils/AuthorizationUtils";
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
const msalInstance = getMsalInstance();
const msalInstance = await getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0];
const cachedTenantId = localStorage.getItem("cachedTenantId");
@@ -18,6 +18,13 @@ interface ReturnType {
tenantId: string;
account: msal.AccountInfo;
switchTenant: (tenantId: string) => void;
authFailure: AadAuthFailure;
}
export interface AadAuthFailure {
failureMessage: string;
failureLinkTitle?: string;
failureLinkAction?: () => void;
}
export function useAADAuth(): ReturnType {
@@ -28,6 +35,7 @@ export function useAADAuth(): ReturnType {
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
const [graphToken, setGraphToken] = React.useState<string>();
const [armToken, setArmToken] = React.useState<string>();
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
msalInstance.setActiveAccount(account);
const login = React.useCallback(async () => {
@@ -61,24 +69,60 @@ export function useAADAuth(): ReturnType {
[account, tenantId],
);
React.useEffect(() => {
if (account && tenantId) {
Promise.all([
msalInstance.acquireTokenSilent({
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
}),
msalInstance.acquireTokenSilent({
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
}),
]).then(([graphTokenResponse, armTokenResponse]) => {
setGraphToken(graphTokenResponse.accessToken);
setArmToken(armTokenResponse.accessToken);
const acquireTokens = React.useCallback(async () => {
if (!(account && tenantId)) {
return;
}
try {
const armToken = await acquireTokenWithMsal(msalInstance, {
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
});
setArmToken(armToken);
setAuthFailure(null);
} catch (error) {
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
// 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
// link is a user action so it will work even if popups have been disabled.
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/76#issuecomment-324787539
setAuthFailure({
failureMessage:
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease click below to retry authorization without requiring popups being enabled.",
failureLinkTitle: "Retry Authorization",
failureLinkAction: acquireTokens,
});
} else {
const errorJson = JSON.stringify(error);
setAuthFailure({
failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`,
});
}
}
try {
const graphToken = await acquireTokenWithMsal(msalInstance, {
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
});
setGraphToken(graphToken);
} catch (error) {
// Graph token is used only for retrieving user photo at the moment, so
// it's not critical if this fails.
console.warn("Error acquiring graph token: " + error);
}
}, [account, tenantId]);
React.useEffect(() => {
if (account && tenantId && !authFailure) {
acquireTokens();
}
}, [account, tenantId, acquireTokens, authFailure]);
return {
account,
tenantId,
@@ -88,5 +132,6 @@ export function useAADAuth(): ReturnType {
login,
logout,
switchTenant,
authFailure,
};
}

View File

@@ -6,6 +6,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap
import { useSelectedNode } from "Explorer/useSelectedNode";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react";
@@ -35,7 +36,7 @@ import {
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
import { acquireTokenWithMsal, getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
@@ -243,16 +244,19 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
let keys: DatabaseAccountListKeysResult = {};
if (account.properties?.documentEndpoint) {
const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
const msalInstance = getMsalInstance();
const msalInstance = await getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount);
const cachedTenantId = localStorage.getItem("cachedTenantId");
const aadTokenResponse = await msalInstance.acquireTokenSilent({
forceRefresh: true,
scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
});
aadToken = aadTokenResponse.accessToken;
try {
aadToken = await acquireTokenWithMsal(msalInstance, {
forceRefresh: true,
scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${cachedTenantId}`,
});
} catch (authError) {
logConsoleError("Failed to acquire authorization token: " + authError);
}
}
try {
if (!account.properties.disableLocalAuth) {