Quote of the Day

more Quotes

Categories

Buy me a coffee

  • Home>
  • security>

Azure AD authentication in angular using MSAL angular v2 library

In previous projects, I use Oidc-client-js to authenticate users against azure AD. Oidc-client-js is a great library but is no longer maintained by the main author. Because of this, I have switched to MSAL angular v2 in my current project. Microsoft provides good documentation and sample projects to help developers to integrate the library into their project. I am able to follow the sample project to get authentication working in my angular application, albeit a few hiccups along the way. In this post, I share some of the issues I ran into and how I structure the codes for authentication.

MSAL angular v2 support for RxJS 7 in angular 13

When I first installed MSAL angular v2, I ran into an issue because MSAL angular v2 requires RxJS ^6.5.3 whereas angular 13has a dependency on RxJS ~7.4.0. In my case, I was able to simply downgrade RxJS version to 6 to get the installation working. As of this writing, this is no longer an issue because current MSAL angular v2 supports RxJS v7. In my project, I have angular 13, MSAL angular ^2.1.2, and RxJS ~7.4.0; these dependencies work well together.

MSAL angular v2 dynamic configurations

In the example projects and documentations, the authentication configurations such as client id, tenant id, redirect url etc. are hardcoded in the app.module file. It may not be apparent where to find the documents that discuss about dynamic configurations. If you are looking for such documents, checkout this this link.

In my case, I tried to use factory providers and app configurations to retrieve the configurations from API before loading the app. However, for some reasons, I could not get it to work. I ended up using platformBrowserDynamic to load the configurations from the API and have those ready before setting up the application.

Following the guide in the document, I modify the main.ts file to fetch the configurations from the backend API, as shown below:

const environmentConfig = new EnvironmentConfig(environment.apiPath, environment.VersionNumber);

export function getBaseUrl() {
  return document.getElementsByTagName('base')[0].href;
}

export function MSALInterceptorConfigFactory(authConfig: AuthenticationConfiguration): MsalInterceptorConfiguration {
  const rootApiUrl = environmentConfig.getRootUrl(getBaseUrl());
  const protectedResourceMap = new Map<string, Array<string> | null>();
  protectedResourceMap.set(`${rootApiUrl}`, authConfig.scopes);

  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap,
  };
}

export function MSALGuardConfigFactory(authConfig: AuthenticationConfiguration): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    authRequest: {
      scopes: authConfig.scopes
    },
    loginFailedRoute: "./login-failed"
  };
}

export function MsalInstanceFactory(authConfig: AuthenticationConfiguration): PublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: authConfig.clientId,
      authority: authConfig.authority,
      redirectUri: authConfig.redirectUri,
      postLogoutRedirectUri: authConfig.postLogoutRedirectUri
    },
    cache: {
      cacheLocation: 'localStorage',
      storeAuthStateInCookie: isIE, // Set to true for Internet Explorer 11
    },
    system: {
      loggerOptions: {
        loggerCallback: (logLevel, message) => { console.log(message) },
        piiLoggingEnabled: true
      }
    }
  })
}


if (environment.production) {
  enableProdMode();
}

fetch(`${environmentConfig.getAuthenticationConfigurationsUrl(getBaseUrl())}`)
  .then(response =>
    response.json()).then(json => {
      let authConfig = new AuthenticationConfiguration(json);
      if (!authConfig.redirectUri) {
        authConfig.redirectUri = getBaseUrl(); 
      }
      if (!authConfig.postLogoutRedirectUri) {
        authConfig.postLogoutRedirectUri = getBaseUrl();
      }
      const providers = [
        { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] },
        { provide: AuthenticationConfiguration, useValue: authConfig },
        { provide: EnvironmentConfig, useValue: environmentConfig },
        {
          provide: MSAL_INSTANCE, useFactory: MsalInstanceFactory,
          deps: [AuthenticationConfiguration]
        },
        {
          provide: MSAL_GUARD_CONFIG,
          useFactory: MSALGuardConfigFactory,
          deps: [AuthenticationConfiguration]
        },
        {
          provide: MSAL_INTERCEPTOR_CONFIG,
          useFactory: MSALInterceptorConfigFactory,
          deps: [AuthenticationConfiguration]
        },
 
      ];
      platformBrowserDynamic(providers)
        .bootstrapModule(AppModule)
        .catch((err) => console.error(err));
    });
 

In the above snippets,

  • environmentConfig.getAuthenticationConfigurationsUrl(getBaseUrl()) returns the API endpoint for retrieving the configurations.
  • I configure MSAL to automatically attach the access token for requests against the API using protectedResourceMap. Note that I pass in the value of the scope. This is necessary to ensure the access token carries the appropriate access for calling the API.

const protectedResourceMap = new Map | null>();
protectedResourceMap.set(${rootApiUrl}, authConfig.scopes);

  • For the same reason, in MSALGuardConfigFactory, I also provide the scopes so that the access token has the appropriate audience value for calling the API.

In previous projects, I loaded the configurations via json files in the asset folder and use azure devops to replace the configurations in the json files when deploying to a target environment. However, I find loading the configurations from an API makes deployment a bit simpler. For instance, I can manage the configs from both API and angular app at a same place. For my project, my backend API is an ASP.NET core app, and the authentication configs are in the appsettings files.

  "ClientAuthentication": {
        "ClientId": "{client id of my angular app as registered in azure ad}",
        "Authority": "https://login.microsoftonline.com/{value of client id above}",
        "Scopes": [ "api://{client id of api}/access_as_user" ]
    },

Using a dedicated authentication service to abstract away dependency on MSAL angular

In my angular app, instead of using MSAL service directly in other components, I abstract the authentication process in a service to reduce the impact the library has on my application should I ever need to update the library or switch to a different one. Using the service as an abstraction, i can also simplify the authentication for the rest of the application by exposing only what my app needs. For instance, other components do not need to aware whether login is using redirect or popup, or whether to pass in scopes; the service encapsulates all of those details.

Below snippets show the content of my auth.service file

import { Inject, Injectable, OnDestroy } from '@angular/core';
import { MsalBroadcastService, MsalGuardConfiguration, MsalService, MSAL_GUARD_CONFIG } from '@azure/msal-angular';
import { InteractionStatus, RedirectRequest } from '@azure/msal-browser';
import { Observable, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { AuthUser } from '../models/auth-user';

@Injectable({
  providedIn: 'root'
})
export class AuthService implements OnDestroy {

  private _authenticationContext$: Subject<AuthUser | null>;
  private readonly _destroying$ = new Subject<void>();

  constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
    private broadcastService: MsalBroadcastService, private msalService: MsalService
  ) {
    this._authenticationContext$ = new Subject();
    this._destroying$ = new Subject<void>();
    this.broadcastService.inProgress$.pipe(
      filter((status: InteractionStatus) => status === InteractionStatus.None),
      takeUntil(this._destroying$)
    ).subscribe(() => {
        this.refreshAuthUser();
    })
  }

  get authenticationContext(): Observable<AuthUser | null> {
    return this._authenticationContext$.asObservable(); 
  }

  ngOnDestroy(): void {
    this._destroying$.next(undefined);
    this._destroying$.complete();
  }

  login() {
    if (this.msalGuardConfig.authRequest) {
      this.msalService.loginRedirect({ ...this.msalGuardConfig.authRequest } as RedirectRequest).subscribe(() => {

      });
    } else {
      this.msalService.loginRedirect();
    }
  }

  logout() {
    return this.msalService.logoutRedirect(); 
  }

  private refreshAuthUser() {
    /**
   * If no active account set but there are accounts signed in, sets first account to active account
   * To use active account set here, subscribe to inProgress$ first in your component
   * Note: Basic usage demonstrated. Your app may require more complicated account selection logic
   */
    let activeAccount = this.msalService.instance.getActiveAccount();

    if (!activeAccount && this.msalService.instance.getAllAccounts().length > 0) {
      let accounts = this.msalService.instance.getAllAccounts();
      activeAccount = accounts[0];
      this.msalService.instance.setActiveAccount(activeAccount);
    }
    if (activeAccount) {
      this._authenticationContext$.next({
        name: activeAccount.name,
        id: activeAccount.homeAccountId,
        email: activeAccount.username
      })
    } else {
      this._authenticationContext$.next(null);
    }
  }
}

In the login() method, notice in the call to msal loginRedirect, the authRequest contains the scope info configured in the main.ts file.

this.msalService.loginRedirect({ …this.msalGuardConfig.authRequest } as RedirectRequest).

AuthenticationConfiguration and AuthUser contain properties to hold the authentication configurations and user info respectively.

export class AuthenticationConfiguration {
  clientId!: string;
  authority!: string;
  redirectUri!: string;
  postLogoutRedirectUri!: string;
  scopes!: string[];

  constructor(init?: Partial<AuthenticationConfiguration>) {
    Object.assign(this, init);
  }
}

export interface AuthUser {
  firstName?: string | undefined;
  lastName?: string | undefined;
  id: string;
  email: string;
}

Overall, I like how MSAL angular v2 abstract away much of the complexity of authentication. I am also grateful that I did not faced any major issues like the ones I ran into when I tried MSAL angular v1 a few years ago. The amount of codes I have to write to integrate login is not much, and a considerable portion of the codes are just configurations. If you are looking to implement login against azure AD in your angular application, check out the library.

References

MSAL Angular v2 Configuration

Angular 13 RxJS7 MSAL Angular v2 Sample

MSAL Angular support Angular 13 and RxJS v7 

2 comments