AzureAD / microsoft-authentication-library-for-js

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

Msal browser getActiveAccount and getAllAccounts are empty #6836

Closed Issa-projects closed 9 months ago

Issa-projects commented 10 months ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

3.7.0

Wrapper Library

Not Applicable

Wrapper Library Version

None

Public or Confidential Client?

Public

Description

I am working on developing a new outlook addin (taskpane) with vue 3 that will ask the user to choose and log in with one of their accounts. For the login , the user clicks "Get started" button, a dialog opens (Office dialog api) where the user chooses an account. When they log in i use messageParent from the Office.context.ui to communicate with the main component. The main component receives the message from the dialog and store it in vuex state manager. msal browser works just fine while the taskpane is open. I get the authenticated user and the access token, but when i close the taskpane, and open again, and try to check if there is an active account, the gerAcctiveAccount and getAllAccounts return empty/null. I noticed when checking the local storage that there are only two enteries: msal.[tenant_id].active-account-filters msal.[tenant_id].active-account

Error Message

no errors

MSAL Logs

No response

Network Trace (Preferrably Fiddler)

MSAL Configuration

const msalConfig = {
    auth: {
        clientId: import.meta.env.VITE_APP_MSAL_CLIENT_ID!,
        redirectUri: "https://localhost:3000/dialog.html",
        authority: "https://login.microsoftonline.com/common",

    },

    cache: {
        cacheLocation: 'localStorage',
        storeAuthStateInCookie: true,
    },

    system: {
        loggerOptions: {
            loggerCallback: (level: LogLevel, message: string, containsPii: boolean) => {
                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;
                }
            },
            logLevel: LogLevel.Verbose
        }
    }
};

Relevant Code Snippets

//loginRequest

const loginRequest = {
    scopes: ['User.Read'],
    prompt: 'select_account',

};

//dialog.ts

window.Office.onReady(async () => {

    const app = createApp(App as any);
    await msalInstance.initialize()

    app.directive("click-outside", ClickOutside)
    app.use(msalPlugin, msalInstance);

    app.mount('#app');
});

//dialog trigger code
await instance.handleRedirectPromise()
      .then((response:any) => {
        // If response is non-null, it means page is returning from AAD with a successful response
        if (response) {
          instance.setActiveAccount(response.account)

          Office.context.ui.messageParent( JSON.stringify({ status: 'success', payload : response }) );
        } else {
          console.log({catch: response})

          instance.loginRedirect(loginRequest);
        }
      })
      .catch((error) => {
        Office.context.ui.messageParent( JSON.stringify({ status: 'error', payload: error }));
      });

Reproduction Steps

  1. Create new vite project with vue 3 typescript
  2. dependencies
    • @azure/msal-browser (3.7.0)
    • vuex (latest)
      1. create two apps, one for rendering the addin content, and one for the dialog

Expected Behavior

array og all the accounts and get active account should return the logged in user account

Identity Provider

Entra ID (formerly Azure AD) / MSA

Browsers Affected (Select all that apply)

Other

Regression

No response

Source

Internal (Microsoft)

tnorling commented 9 months ago

Sounds like each instance of the dialog has its own storage - if you manually put something in localstorage is it still there in the 2nd dialog?

If that's the case I'd recommend calling the MSAL APIs in your main frame so cache is stored there instead. You can use the onRedirectNavigate request parameter on loginRedirect to open the dialog and perform the interaction there, then message the url hash back to your main frame and have your main frame invoke handleRedirectPromise with that hash.

Issa-projects commented 9 months ago

@tnorling could you please give example code on how this would be implemented?

tnorling commented 9 months ago

Rough pseudo code, you'll need to fill in the blanks with the specific APIs you need to use


// Main Window
onMessageFromDialog((responseHash) => {
  // Message received from dialog
  pca.handleRedirectPromise(responseHash);
});

pca.loginRedirect({
  scopes: [User.Read], 
  onRedirectNavigate: (url) => {
    // Open dialog and send it the url
    return false; // Prevent main window from attempting to redirect
  }
}); 

// Dialog box
if (window.location.hash) {
  // Returning from auth flow
  sendMessageBacktoParent(window.location.hash);
  closeDialog();
} else {
  // Beginning of auth flow, redirect
  window.location.href = url; // url from parent
}
Issa-projects commented 9 months ago

This does not work since the dialog should prompt the user to login/select account and then closes from the parent on success login. I will show what i am currentlig doing:

Actions to trigger and comunicate with the dialog:


async dialog({commit, dispatch, state}, refresh = false) {
            const dialogLoginUrl = import.meta.env.VITE_APP_MSAL_DIALOG_URI;
            let loginDialog: Office.Dialog;
            await commit("setLoading", true);
            Office.context.ui.displayDialogAsync(
                dialogLoginUrl,
                {height: 45, width: 25},
                async (result) => {
                    loginDialog = result.value;
                    if (result.status === Office.AsyncResultStatus.Failed) {
                        loginDialog.close()

                        commit("setAlert", {message: "message", type: "error"});
                    } else {
                        loginDialog.addEventHandler(Office.EventType.DialogMessageReceived, processLoginMessage);
                        loginDialog.addEventHandler(Office.EventType.DialogEventReceived, processDialogEvent);

                    }

                }
            );
            const processDialogEvent = async (arg:any) => {
                console.log({arg})
                if(arg.error == 12006){
                    loginDialog.close()
                }
            }

            const processLoginMessage = async (arg: { message: string, origin: string } | { error: number }) => {

                if ("error" in arg) {
                    commit("setAlert", {message: "message", type: "error"});

                    return;
                }
                const message = JSON.parse(arg.message);
                if (message.status == "error") {
                    commit("setAlert", {message: "message", type: "error"});

                    return;
                }
                   /**
                    *  login success. Parse the success message
                    */
                 loginDialog.close()

            };

          await commit("setLoading", false);

        },```

The dialog:

```typescript
<template>
  <div class="flex items-center justify-center h-full w-full p-3" >
    <Loader size="lg" ></Loader>
  </div>
</template>

<script lang="ts" setup>

import {loginRequest} from "@/config/authConfig";
import {useMsal} from "@/utils/useMsal";
import {onMounted} from "vue";
import Loader from "@/Components/Loader.vue";
const { instance } = useMsal();

const login = async () => {

  await instance.handleRedirectPromise()
      .then((response:any) => {
        // If response is non-null, it means page is returning from AAD with a successful response
        if (response) {
          instance.setActiveAccount(response.account)

          Office.context.ui.messageParent( JSON.stringify({ status: 'success', payload : response }) );
        } else {
          console.log({catch: response})

          instance.loginRedirect(loginRequest);
        }
      })
      .catch((error) => {
        Office.context.ui.messageParent( JSON.stringify({ status: 'error', payload: error }));
      });

}
login();

</script>```       
tnorling commented 9 months ago

That's the point of the onRedirectNavigate parameter. This enables the dialog to handle user prompts while still allowing the parent to do the rest of the heavy lifting and hold the token cache

tnorling commented 9 months ago

async dialog({commit, dispatch, state}, refresh = false) {
            const dialogLoginUrl = import.meta.env.VITE_APP_MSAL_DIALOG_URI;
            let loginDialog: Office.Dialog;
            await commit("setLoading", true);
            pca.loginRedirect({...loginRequest, onRedirectNavigate: (url) => {

            Office.context.ui.displayDialogAsync(
                url,
                {height: 45, width: 25},
                async (result) => {
                    pca.handleRedirectPromise(result.value);
                    loginDialog.close()
                }
            );
            return false;
            }});

          await commit("setLoading", false);

        },

The dialog:

```typescript
<template>
  <div class="flex items-center justify-center h-full w-full p-3" >
    <Loader size="lg" ></Loader>
  </div>
</template>

<script lang="ts" setup>

Office.context.ui.messageParent( JSON.stringify({ status: 'success', payload : window.location.hash }) );
</script>
Issa-projects commented 9 months ago

I am getting an error indicating that the dialog couldn't open with that url:

Error: "The domain of the URL is not included in the AppDomains element in the manifest, and is not subdomain of source location."

I see the onRedirectNavigate callback url is the authorize login url, but the dialog expected a url to open on the same domain.

Issa-projects commented 9 months ago

I added the domain https://login.microsoftonline.com to appDomains in the manifest file, and it looks like it is working. Thanks for help @tnorling