AzureAD / microsoft-authentication-library-for-js

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

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

Closed tiwarigaurav closed 6 months ago

tiwarigaurav commented 1 year ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

2.32.2

Wrapper Library

Not Applicable

Wrapper Library Version

None

Public or Confidential Client?

Public

Description

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.

MSAL Configuration

auth: {
        authority: "https://login.microsoftonline.com/organizations/",
        clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
      },
cache: {
        cacheLocation: "localStorage",
        storeAuthStateInCookie: true,
       }

Relevant Code Snippets

No response

Identity Provider

None

Source

External (Customer)

sameerag commented 1 year ago

Can you please check the scopes docs and let us know if it helps?

tiwarigaurav commented 1 year ago

@sameerag - I have already gone over this document and this does not help. We cannot combine Graph and SharePoint scopes in one request. Even if we could somehow (using a hack), this does not solve the problem i.e. when the access token expires (which is 1 hour) user will need to again click a button to initiate the popup and get another token.

Also pointing out the below text from my initial query -

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.

Getting a token for multiple resources can be achieved in the login.ts implementation (see code snippet below) - the main issue is that the user will need to login every 1 hour which is not ideal in a production app.

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) {

                const authParamsSharePoint = {
                  scopes: ["https://tenant.sharepoint.com/.default"]
                };

                let spResp = await msalInstance.acquireTokenSilent(authParamsSharePoint);

                Office.context.ui.messageParent(JSON.stringify({ status: 'success', result : response }));
            } else {
                msalInstance.loginRedirect(authParamsGraph);
            }
        })
        .catch(() => {
            console.log('failure');
        });
    };
})();
sameerag commented 1 year ago

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.

Why is interaction needed every one hour? You should have a refresh token in cache, which should help in renewing the access token silently with acquireTokenSilent. Also, you cannot combine two different scopes for access_token but can combine them for consent in the initial login click and then do separate acquireToken calls for fetching the corresponding tokens.

tiwarigaurav commented 1 year ago

Why is interaction needed every one hour? You should have a refresh token in cache, which should help in renewing the access token silently with acquireTokenSilent.

@sameerag - Yes, you are right for when the app is running in Outlook on the browser - but this is not true when the app is running on Outlook desktop application (Windows). It seems that the msalInstance does not have access to the cache from the popup. 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 REACT cannot share the context or variables between the login popup and the app.

Also, you cannot combine two different scopes for access_token but can combine them for consent in the initial login click and then do separate acquireToken calls for fetching the corresponding tokens.

Do you have any documentation or code samples for this?

tiwarigaurav commented 1 year ago

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

tiwarigaurav commented 1 year ago

@sameerag - any updates for us in this issue?

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

microsoft-github-policy-service[bot] commented 1 year ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @sameerag please follow up.

sameerag commented 6 months ago

Apologies for missing to close this loop.

MSAL JS is explicitly for web, SPA experience. For desktop, the Office desktop folks probably are the best folks to help address this. Since the popup is doing the auth inside itself (with a redirect), MSAL JS cannot retrieve its cache to enable silent auth, as the popup tearing down will tear the MSAL cache too.

sameerag commented 6 months ago

Closing this. If the issue is still unsolved, please raise a new issue. Thanks.