Search code examples
typescriptvuejs2azure-ad-msalmsal

authentification using a custom service


I am trying to implement a login using the azure-msal-browser package in a Vue2 application.

I am struggling to get it running even after checking a couple of different tutorials.

My plan is as follows: (a) i want to have a seperate service which contains the login and getbearertoken method (b) this service is initalized in the beforeCreate() of my Vue instance so that i can only access the page after login and (c) the get tokenmethod should be usable from any other .ts file in my project so that i can append this to an post / get request.

So far my login is working, but i am struggling to get the token:

import { msalConfig, useAuthStore } from '@/stores/auth'
import { PublicClientApplication } from '@azure/msal-browser'

class MsalService {
  msalInstance: PublicClientApplication

  constructor() {
    this.msalInstance = new PublicClientApplication(msalConfig)
    this.initialize()
  }

  async initialize() {
    try {
      await this.msalInstance.initialize()
    } catch (error) {
      console.log('Initialization error', error)
    }
  }

  login() {
    const authStore = useAuthStore()

    this.msalInstance
      .handleRedirectPromise()
      .then((response: any) => {
        if (!response) {
          this.msalInstance.loginRedirect()
        } else {
          authStore.setUser(response.account)
        }
      })
      .catch((error: any) => {
        console.error('Failed to handle redirect:', error)
      })
  }


  async getToken(): Promise<any> {
    try {
      const accounts = this.msalInstance.getAllAccounts()
      if (accounts.length === 0) {
        throw new Error('No accounts found. Please login first.')
      }
      const silentRequest = {
        scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
        account: accounts[0]
      }
      const silentResponse = await this.msalInstance.acquireTokenSilent(silentRequest)
      return silentResponse.accessToken
    } catch (error) {
      console.error('Token acquisition error:', error)
      throw error
    }
  }
}

export const msalService = new MsalService()

In my main.ts the code looks like this

import { msalService } from "@/stores/auth"

export const instance = new Vue({
  router,
  ...
  beforeCreate() {

    msalService.initialize().then(() => {
      msalService.login()
    })

  }
}

console.log(await msalService.getToken())

this getToken() call returns a "BrowserAuthError: uninitialized_public_client_application: You must call and await the initialize function before attempting to call any other MSAL API" error in the console.

As mentioned, the login works and i am asked to input my credentials. What might be the issue?


Solution

  • I tried the below Vue2 application using the @azure/msal-browser package to log in and retrieve the access token. I successfully logged in and retrieved the access token.

    src/services/msalservice.ts :

    import { PublicClientApplication, AuthenticationResult } from '@azure/msal-browser';
    import { config } from '../Config';
    
    class MsalService {
      private msalInstance: PublicClientApplication;
      private isInitialized: boolean = false;
    
      constructor() {
        this.msalInstance = new PublicClientApplication({
          auth: {
            clientId: config.appId,
            redirectUri: config.redirectUri,
            authority: config.authority,
          },
          cache: {
            cacheLocation: 'sessionStorage',
            storeAuthStateInCookie: true,
          },
        });
      }
    
      public async initialize(): Promise<void> {
        try {
          await this.msalInstance.initialize();
          this.isInitialized = true;
          console.log('MSAL instance initialized successfully');
        } catch (error) {
          this.isInitialized = false;
          console.error('Error initializing MSAL instance:', error);
          throw error;  
        }
      }
      private checkInitialization() {
        if (!this.isInitialized) {
          throw new Error("MSAL has not been initialized yet.");
        }
      }
    
      public getMsalInstance() {
        this.checkInitialization();
        return this.msalInstance;
      }
    
      public async login(): Promise<AuthenticationResult | null> {
        this.checkInitialization();
        try {
          const loginResponse = await this.msalInstance.loginPopup({
            scopes: config.scopes,
            prompt: 'select_account',
          });
          return loginResponse;
        } catch (error) {
          console.error(error);
          return null;
        }
      }
    
      public async getAccessToken(): Promise<string | null> {
        this.checkInitialization();
        try {
          const account = this.msalInstance.getAllAccounts()[0];
          if (account) {
            const tokenResponse = await this.msalInstance.acquireTokenSilent({
              scopes: config.scopes,
              account: account,
            });
            return tokenResponse.accessToken;
          }
          return null;
        } catch (error) {
          console.error(error);
          return null;
        }
      }
      
      public logout(): void {
        this.checkInitialization();
        this.msalInstance.logout();
      }
    }
    export const msalService = new MsalService();
    

    src/components/App.vue :

    <template>
        <div id="app">
          <h1>MSAL Vue 2 App</h1>
          <div v-if="!account">
            <button @click="login">Login</button>
          </div>
          <div v-else>
            <p>Welcome, {{ account.name }}</p>
            <p v-if="accessToken">
              <strong>Access Token:</strong>
              <pre>{{ accessToken }}</pre>
            </p>
            <button @click="logout">Logout</button>
          </div>
        </div>
      </template>
      
      <script lang="ts">
      import Vue from 'vue';
      import { msalService } from '../services/msalService';
      
      export default Vue.extend({
        data() {
          return {
            account: null as any,
            accessToken: null as string | null,
            isMsalInitialized: false,
          };
        },
        async mounted() {
          try {
            await msalService.initialize();
            this.isMsalInitialized = true;
            console.log('MSAL instance initialized');
            const msalInstance = msalService.getMsalInstance();
            const accounts = msalInstance.getAllAccounts();
            if (accounts.length > 0) {
              this.account = accounts[0];
              await this.fetchAccessToken();
            }
          } catch (error) {
            console.error('Error initializing MSAL:', error);
          }
        },
        methods: {
          async login() {
            if (!this.isMsalInitialized) {
              console.error('MSAL is not initialized yet.');
              return;
            }
            try {
              const response = await msalService.login();
              if (response) {
                this.account = response.account;
                await this.fetchAccessToken();
              }
            } catch (error) {
              console.error('Login error:', error);
            }
          },
          async fetchAccessToken() {
            try {
              this.accessToken = await msalService.getAccessToken();
            } catch (error) {
              console.error('Error fetching access token:', error);
            }
          },
          logout() {
            msalService.logout();
            this.account = null;
            this.accessToken = null;
          }
        },
      });
      </script>
    

    src/Config.ts :

    export const config = {
      appId: '<clientID>',
      redirectUri: 'http://localhost:8080',
      scopes: ['openid', 'profile', 'user.read'],
      authority: 'https://login.microsoftonline.com/<tenantID>',
    };
    

    main.ts :

    import Vue from 'vue';
    import App from './components/App.vue';  
    
    Vue.config.productionTip = false;
    new Vue({
      render: (h) => h(App),
    }).$mount('#app');
    

    I have added the below redirect URI in the service principle's Authentication under Single-page application.

    http://localhost:8080
    

    enter image description here

    Output :

    I successfully ran the Vue2 project and got the below output in the browser.

    enter image description here

    I successfully logged in and retrieved the access token.

    enter image description here