Migrate Angular SPA from ADALJS to MSAL because it is awesome

jon-tyson-420512-unsplash-resized.jpg

This is a public service announcement for all office devs.

If you are using ADALJS - you need to upgrade your project to MSAL. It is awesome. It has everything you want, and it worked the way we expected it to, right out of the box. Plus a bunch more new features.

It is 2018, we can finally put away ADALJS. Do it ASAP.


Plan

  • High level ideas - replace A with B

  • New things we can now do

  • Code diff snippets (if you want to compare notes with your own)

High Level

ADALJS was never released stand-alone. It was released as part of an Adal-AngularJS library. In the many years after several community produced wrappers were created to wrap ADALJS into various frameworks. The one I was using is ng2-adal. But there are others.

  • AdalService -> MsalService

  • this.adalService.userInfo -> this.msalService.getUser()

  • replace auth route guards with MsalGuard

  • move adalService.config(adalConfig) to MsalModule imports dependency injection

  • add msalInterceptor to HTTP_INTERCEPTORS which automatically attach the correct bearer token

  • switch http (from httpModule) to httpClient (from httpClientModule) which listens to HTTP_INTERCEPTORS

  • a handy tip to detect if SPA is running inside an adalFrame and disable route-outlets (this disables sub-components from loading inside the iframe - this is a great tip for adaljs as well)

MSAL provides three libraries with examples - make sure you switch to the relevant library instead.

MSAL is Awesome!

  • Listen to BroadcastService observables to be notified when tokens are received successfully or expired, or if consent level has changed.

  • MSAL supports incremental consent.

  • Can use acquireTokenSilent to obtain tokens silently, listen to event subscription to catch if grant isn’t available - then call acquireTokenRedirect or acquireTokenPopUp.
    Generally listen to “consent_required” or “interaction_required”.

  • App should be registered with the set of permissions that an admin can grant for the whole organization.

  • But MSAL can request additional consent separately. Users will have to grant those.

  • MSAL can issue both v1 and v2 tokens so it has no problems talking to APIs that still need v1 tokens.

  • Switch library has no visible effect to your end users - they will not see any new consent and everything will happen silently in the back.

The conversion took me two evenings to wrap up. I’m now in the process of adding new incremental consent.

Code Diff Snippets

While the whole thing is still fresh in my mind, I want to write this blog post.

Here’s what I start with

  • Angular 7

  • ng2-adal

  • adal-angular (includes adal.js)

I end up with

  • @azure/msal-angular

npm

npm install @azure/msal-angular --save

npm uninstall ng2-adal adal-angular --save-dev

app.module.ts

Change imports to msal-angular, remove my own auth.guard and use MsalGuard instead.

Use MsalIntercepter to automatically add bearer token on HttpClient calls (so replacing HttpModule’s Http with the new HttpClientModule’s HttpClient).

Initialize MsalModule with config (this is traditionally the adalConfig.

// remove
import { AdalService } from 'ng2-adal/dist/core';
import { AuthGuard } from "./my/auth.guard";
// replace
import { MsalModule } from "@azure/msal-angular";
import { MsalInterceptor } from "@azure/msal-angular";
import { LogLevel } from 'msal';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';

export function loggerCallback(logLevel, message, piiEnabled) {
    console.log("client logging" + message);
}
export const protectedResourceMap: [string, string[]][] = [
    ['https://graph.microsoft.com/v1.0/me', ['user.read']],
    // ... other scopes
];

// imports
imports: [
    ...
    // HttpModule // remove
    HttpClientModule, // replace
    MsalModule.forRoot({
        clientID: '00000000-0000-0000-0000-000000000000',
        authority: "https://login.microsoftonline.com/common/",
        validateAuthority: true,
        redirectUri: window.location.origin,
        cacheLocation: 'localStorage',
        postLogoutRedirectUri: window.location.origin,
        navigateToLoginRequestUrl: false,
        popUp: false,
        unprotectedResources: ["https://www.microsoft.com/en-us/"],
        protectedResourceMap: protectedResourceMap,
        logger: loggerCallback,
        correlationId: "1000",
        level: LogLevel.Info,
        piiLoggingEnabled: true
    })
    ....
],
providers: [
    // AdalService,  // remove
    // AuthGuard,    // remove
    {provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true}   // add
    ],

app.component.ts

constructor(
  ...
  //private adalService: AdalService,
  private broadcastService: BroadcastService,
  private authService: MsalService
) {
  this.isIframe = window !== window.parent && !window.opener;
  //this.adalService.init(this.secretService.adalConfig);
  //this is moved to app.module.ts
}

ngOnInit(): void {
  this.subscription = this.broadcastService.subscribe("msal:loginSuccess", 
    (payload) => {
      console.log("login success " + JSON.stringify(payload));    
      this.loggedIn = true;
      this.user = this.msalService.getUser();
    });  
}

ngOnDestroy() {
  // disconnect from broadcast service on component destroy
  this.broadcastService.getMSALSubject().next(1);
  if (this.subscription) {
    this.subscription.unsubscribe();
  }
}

app.component.html

<div>
  ...
  <router-outlet style="text-align:center;" *ngIf="!isIframe"></router-outlet>
</div>

app.routing-module.ts

...
import { MsalGuard } from '@azure/msal-angular';

const routes: Routes = [
    { path: '', 
        component: DemoComponent,
        canActivate: [MsalGuard],
        children: [
          ...
        ]
    },
];

Summary

TLDR - the first two paragraphs outline the high level changes we need to apply.

Sorry for the big massive screens and screens of text - these code are from my Flow Studio - and I have to cut out parts of the code to show specific changes that we need to make.