OfficeDev / office-js

A repo and NPM package for Office.js, corresponding to a copy of what gets published to the official "evergreen" Office.js CDN, at https://appsforoffice.microsoft.com/lib/1/hosted/office.js.
https://learn.microsoft.com/javascript/api/overview
Other
683 stars 95 forks source link

How can I get access token for multiple resources in an Outlook Web AddIn running in Outlook Desktop App #3633

Open tiwarigaurav opened 1 year ago

tiwarigaurav commented 1 year ago

I am developing an Outlook Web Add-In using

Yeoman generator-office Office.Js REACT framework azure/msal-browser version 2.32.2

As per Microsoft/Office Add-In/MSAL team's best practice - I am performing a popup auth in the office dialogue using the office SDK, and then perform loginRedirect inside that popup. In my App.tsx file I have the following code to do the popup:

const dialogLoginUrl: string = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + '/login.html';

await Office.context.ui.displayDialogAsync(
  dialogLoginUrl,
  {height: 40, width: 30},
  (result) => {
      if (result.status === Office.AsyncResultStatus.Failed) {
      }
      else {
          loginDialog = result.value;
          loginDialog.addEventHandler(Office.EventType.DialogMessageReceived, this.processLoginMessage);
      }
  }
);

processLoginMessage = async (args: { message: string; origin: string; }) => {
  let messageFromDialog = JSON.parse(args.message);
  if (messageFromDialog.status === 'success') {
    loginDialog.close();
    console.log(messageFromDialog.result.accessToken);    
  }
  else {
    // Something went wrong with authentication or the authorization of the web application.
    loginDialog.close();
  }
}

In my login.ts file I have the following code:

import { PublicClientApplication } from "@azure/msal-browser";

(() => {
    Office.initialize = () => {
        let msalInstance = new PublicClientApplication({
            auth: {
            authority: "https://login.microsoftonline.com/organizations/",
            clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
            },
            cache: {
            cacheLocation: "localStorage",
            storeAuthStateInCookie: true,
            },
        });

        const authParamsGraph = {
            scopes: ["https://graph.microsoft.com/.default"]
        };
         msalInstance.handleRedirectPromise()
        .then(async (response) => {
            if (response) {
                Office.context.ui.messageParent(JSON.stringify({ status: 'success', result : response }));
            } else {
                msalInstance.loginRedirect(authParamsGraph);
            }
        })
        .catch(() => {
            console.log('failure');
        });
    };
})();

Using the above setup I can get the Graph Access Token just fine and everything works.

How can I get access token for multiple resources using the msalInstance.acquireTokenSilent(scope) within the app.ts file?

What I have been able to do so far is to call msalInstance.acquireTokenSilent(sharepointScopes) in the login.ts file and get the SharePoint Access Token too. However this is not ideal in a production environment as the access token expires in 1 hour, so the user will again need to click a button to initiate the popup to get another token.

The main idea is to use the login popup once to initiate the msalinstance and for any subsequent scopes/resources (or when the initial access token expires) use the acquireTokenSilent method - so that the user does not need to click the login button again to get another token.

What I have seen is - if I can get the msal instance back from the popup to the react app then I can reuse it to call "acquireTokenSilent" - which will solve the issue.

tiwarigaurav commented 1 year ago

@exextoc Please let me know if you need more details in investigating our issue. We anticipate quick response and resolution to above.

exextoc commented 1 year ago

Which Outlook Client are you using? Outlook on Windows/ New Outlook on Windows/Outlook Web / Outlook Mac / Outlook iOS/Andriod?

tiwarigaurav commented 1 year ago

I am using Outlook desktop app on Windows.

Some other things I have tried so far:

What I did is tried to get the acquireTokenSilent in the app.ts file after the login popup (see below).

const dialogLoginUrl: string = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + '/login.html';

await Office.context.ui.displayDialogAsync(
  dialogLoginUrl,
  {height: 40, width: 30},
  (result) => {
      if (result.status === Office.AsyncResultStatus.Failed) {
      }
      else {
          loginDialog = result.value;
          loginDialog.addEventHandler(Office.EventType.DialogMessageReceived, this.processLoginMessage);
      }
  }
);

processLoginMessage = async (args: { message: string; origin: string; }) => {
  let messageFromDialog = JSON.parse(args.message);
  if (messageFromDialog.status === 'success') {
    loginDialog.close();
    console.log(messageFromDialog.result.accessToken);    

    let msalInstance: PublicClientApplication = new PublicClientApplication({
      auth: {
      authority: "https://login.microsoftonline.com/organizations/",
      clientId: "26431e25-8afe-44de-8ff3-43c6e89e8d86",
      navigateToLoginRequestUrl: false
      },
      cache: {
      cacheLocation: "localStorage",
      storeAuthStateInCookie: true,
      },
    });
    const authParamsSharePoint = {
      account: messageFromDialog.result.account,
      scopes: ["https://tenant_name.sharepoint.com/.default"]
    };

    // I get an error here
    let spResp = await msalInstance.acquireTokenSilent(authParamsSharePoint);
    console.log(spResp);

  }
  else {
    // Something went wrong with authentication or the authorization of the web application.
    loginDialog.close();
  }
}

Using the above I get the following error:

Exception has occurred: ClientAuthError: token_refresh_required: Cannot return token from cache because it must be refreshed. This may be due to one of the following reasons: forceRefresh parameter is set to true, claims have been requested, there is no cached access token or it is expired.

Note: that I need to initiate a new instance of the PublicClientApplication as we cannot share the context or variables between the login popup and the app as per Microsoft documentation:

The Office dialog API, specifically the displayDialogAsync method is a completely separate browser instance from the task pane, meaning:

  • It has its own runtime environment and window object and global variables.
  • There is no shared execution environment with the task pane.
  • It does not share the same session storage (the Window.sessionStorage property) as the task pane.
tiwarigaurav commented 1 year ago

@exextoc - any updates for us on this issue?

zhngx1 commented 1 year ago

You can use the Office.SessionData: https://learn.microsoft.com/en-us/javascript/api/outlook/office.sessiondata?view=outlook-js-preview Or the messageParent method from the displayDialog: https://learn.microsoft.com/en-us/javascript/api/outlook/office.sessiondata?view=outlook-js-preview

You can also alter the lifetime of the token: https://learn.microsoft.com/en-us/azure/active-directory/develop/configurable-token-lifetimes

tiwarigaurav commented 1 year ago

Hi @zhngx1 - This does not help or address the issue. I am already using messageParent to pass the access token back to the task pane - there is no problems with this. The issue is that when the access token expires and we need to get another one the user will need to click the login button again to trigger the "displayDialogAsync" popup since the "acquireTokenSilent" does not work.

zhngx1 commented 1 year ago

If the issue is with "acquireTokenSilent" API not working, then this is not the correct place for reporting the issue since this is not part of the Office-js API for Outlook.

tiwarigaurav commented 1 year ago

@zhngx1 - this is not an issue with "acquireTokenSilent" API, it is an issue with Office-JS.

Office JS, for some reason, not sharing the session/local storage between the "displayDialogAsync" popup and the Task Pane so the "acquireTokenSilent" fails.

zhngx1 commented 1 year ago

@tiwarigaurav You can try to use the single sign-on with MSAL.js. Here is the link: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-js-sso

tiwarigaurav commented 1 year ago

@zhngx1 - Not sure if you read the document and my previous comment. The document talks about using the browser cache (session/local storage) to access the msal instance in order to do the SSO.

Office JS does NOT allow us to share the (cache) session/local storage between the "displayDialogAsync" popup and the Task Pane. So the example that you referenced in the link above does NOT work.

millerds commented 1 year ago

Have you looked at the SSO quick start documentation at: https://learn.microsoft.com/en-us/office/dev/add-ins/quickstarts/sso-quickstart. That add-in project generated by 'yo office' uses the office.js dialog where needed and is able to do the silent auth otherwise. It sounds similar to what you are trying to, except slightly different auth configuration.

tiwarigaurav commented 1 year ago

Hi @millerds

Thanks for sharing this. I tried your suggestion however it results in the same issue - let me explain what I did:

  1. Since I need to use my AAD app for authentication (because of scopes and to be able to work when SSO is not available i.e. OfficeRuntime.auth.getAccessToken) - I am calling the dialogFallback function directly in the sso-helper.ts file
  2. This works fine in the browser (Outlook on the web) but in the Outlook desktop app it always shows the LoginPopup window
  3. It seems that the Outlook Desktop is unable to do a acquireTokenSilent

Going back to my original issue and conclusion - it seems that the browser cache is not shared between the TaskPane and the displayDialogAsync popup.

exextoc commented 1 year ago

@tiwarigaurav

  • it seems that the browser cache is not shared between the TaskPane and the displayDialogAsync popup. This is by design. Cache is not shared between dialog and taskpane.
millerds commented 1 year ago

@tiwarigaurav I'm not sure I follow everything you said . . .

dialogFallback function is already being called from sso-helper.ts . . . but only in the case where there was a problem trying to call the middle tier with existing information.

You should be able to use your own AAD app rather than the one our tooling sets up, but that requires other changes in the code to make sure the right app is called . . . and that the app is setup correctly (I personally get confused each time I go through this).

You'll see that in the dialogFallback function acquireTokenSilent is used and does work in Outlook Desktop . . . it's only used once it's known that auth has happened (we have an account id).

Some more information about office dialog and auth can be found at (not sure if you see this one yet) https://learn.microsoft.com/en-us/office/dev/add-ins/develop/auth-with-office-dialog-api which notes some caching boundaries. between the dialog and the taskpane (different "browser instances").

tiwarigaurav commented 1 year ago

@millerds - I'll try and explain.

Since Office Web Add-In's are supposed to be compatible across browsers and across platforms we cannot rely on SSO (SSO does not work in Safari and other exceptions). So we need the dialogFallback function. For this reason, in my testing I removed the SSO part and directly call the dialogFallback function from sso-helper file.

If you add break points to the dialogFallback function you will see that the acquireTokenSilent never hits and always ends up in the else branch acquireTokenSilent.

Note I am using Outlook Desktop app on Win 10 machine

zhngx1 commented 1 year ago

Thank you @tiwarigaurav for bringing this up. I will verify this and get back to you.

zhngx1 commented 11 months ago

Thanks for reporting this issue regarding acquireTokenSilent never hits in the SSO template. It has been put on our backlog. We unfortunately have no timelines to share at this point.

Internal tracking id: Office: 8583385