Migrate Angular SPA from ADALJS to MSAL because it is awesome
/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.
https://github.com/AzureAD/microsoft-authentication-library-for-js
MSAL-core https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-core/README.md
MSAL-angularjs https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angularjs/README.md
MSAL-angular https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/README.md
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.