AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.66k stars 2.65k forks source link

Handling account selection in Office Add-in #6699

Closed FrancoBimco closed 10 months ago

FrancoBimco commented 11 months ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

3.5.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

2.0.7

Public or Confidential Client?

Public

Description

I am developing an add-in for Outlook and I have several accounts tied to my organization's tenant, so whenever I am trying to log in and set the active account I am stopped since office add-in don't allow redirects in the same window / browser instance as the original taskpane that sent the request.

First error: I try to ask for the accounts that I am logged in to, it work for about 3 weeks but now I am forced to select one of the accounts using an UI, which forces to use an Office dialog. (before I could ask for all of the accounts and I could programatically select an account, but this is no longer available for a reason I can't seem to find out). But once I am on my Dialog window, msal can easily fetch all of my accounts and I can fetch my tokens without any issues.

Second "error": This might not really be an error, but just a lack of understanding from my part. Since I am forced to do an interaction to select the account which the app should fetch the tokens on behalf of, I launch a dialog in which I instantiate an MsalInstance, handle the redirect promise, get the account info and tokens correctly for my account and then I try to send them back to my original calling window through Json message. Once received, I try to either fetch the account using the getAccount with the idToken claims, accountInfo, and other elements from the message sent by my Dialog window, this always returns a null. I also try to set the account by using setActiveAccount using the account info I got in my Dialog window, but this does not work at all.

Error Message

InteractionRequiredAuthError

Msal Logs

No response

MSAL Configuration

msalConfig:
export const msalConfig: Configuration = {
  auth: {
    clientId: process.env.CDS_CLIENT_ID,
    authority: `https://login.microsoftonline.com/${process.env.BIMCO_TENANT_ID}`,
    redirectUri: "https://localhost:3000/auth.html",
    postLogoutRedirectUri: "https://localhost:3000/auth.html",
    navigateToLoginRequestUrl: false,
  },
  system: {
    allowNativeBroker: false, // Disables WAM Broker
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
          default:
            return;
        }
      },
    },
  },
  cache: {
    cacheLocation: "localStorage",
    storeAuthStateInCookie: true,
  },
};

Relevant Code Snippets

export const loginRequest: PopupRequest = {
  scopes: [`${process.env.CDS_API_APP_REGISTRATION_ID_SCOPE}`, "User.Read.All", "GroupMember.Read.All", "openid"],
};

Initialization of my msalInstance:

Office.onReady(() => {
  isOfficeInitialized = true;

  msalInstance.initialize().then(async () => {
    // await msalInstance.handleRedirectPromise();
    const accounts = msalInstance.getAllAccounts();

    if (accounts.length > 0) {
      msalInstance.setActiveAccount(accounts[0]);
    }

    msalInstance.addEventCallback((event: EventMessage) => {
      if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
        console.log("LOGIN_SUCCESS");
        const payload = event.payload as AuthenticationResult;
        const account = payload.account;
        msalInstance.setActiveAccount(account);
      }
      if (event.error?.name === "InteractionRequiredAuthError" && event.eventType === EventType.SSO_SILENT_FAILURE) {
        console.log("interaction_required");

        const Url = "https://localhost:3000/auth.html";
        msalInstance.loginRedirect({
          scopes: loginRequest.scopes,
          prompt: "select_account",
          onRedirectNavigate: (url) => {
            console.log(url);
            Office.context.ui.displayDialogAsync(
              url, // URL of your login page
              { height: 60, width: 30, displayInIframe: true },
              function (asyncResult) {
                if (asyncResult.status === Office.AsyncResultStatus.Failed) {
                  // Handle error
                  console.error("Failed dialog: " + asyncResult.error.message);
                } else {
                  // Dialog opened successfully
                  var dialog = asyncResult.value;
                  dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg) => {
                    processMessage(arg);
                    dialog.close();
                  });
                }
              }
            );
            return false;
          },
        });
      }
    });
  });

--Handling of message coming from child window:
async function processMessage(arg: any) {
  try {
    const response = JSON.parse(arg.message) as AuthenticationResult;

    const accountInfo = response;

    console.log(response);
    console.log(accountInfo);

    await msalInstance.setActiveAccount(response.account);

    //I have tried many ways to set the account by fetching the account again on this instance using tokenClaims, accountInfo, etc. but to no avail 

  } catch (error) {
    console.error("Error processing message from dialog", error);
    // Handle error
  }
}

-----   Child window 

const msalInstance = new PublicClientApplication(msalConfig);

msalInstance.initialize().then(async () => {
  console.log("Auth.tsx");
  const response = await msalInstance.handleRedirectPromise();

  if (response) {
    Office.context.ui.messageParent(JSON.stringify(response));
    window.close();
  } else {
    msalInstance.loginRedirect({
      scopes: LoginScopes,
    });
  }
});

const Auth: React.FC = () => {
  return <div>Authenticating...</div>;
};

Reproduction Steps

  1. Set up an msal instance with multiple accounts available in an office add in
  2. Call a redirect from a dialog
  3. Try to set up the user account in the original task pane msal instance

Expected Behavior

I would have expected that I could use the token claims from my child window to authenticate in my original taskpane and re-use the account info so that I can fetch new tokens, etc... This is a business add-in and it would be very annoying for my users to have to select an account every single time they want to call an API and that their token is expired.

Identity Provider

Azure AD / MSA

Browsers Affected (Select all that apply)

Internet Explorer

Regression

No response

Source

External (Customer)

tnorling commented 11 months ago

I'd try handling the response in the original window instead of in the child window. Don't call any MSAL APIs inside the child window.

  1. Parent window calls loginRedirect with onRedirectNavigate callback configured to open the Office dialog window
  2. When the Office dialog returns to redirectUri, send window.location.hash back to parent window
  3. On message from child, parent window calls handleRedirectPromise()

Doing it this way keeps the cache in the parent window rather than in the ephemeral child window.

microsoft-github-policy-service[bot] commented 10 months ago

@FrancoBimco This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.