Angular - Surface SharePoint List Data With MSGraph - Part 4 - Authentication

Share on:

Overview

This Project was discontinued mid 2021!

Leaving here for reference!

Please Note!

One of the dangers in writing a tutorial like this is that by the time you are finished writing it, there is a new revision to the libraries you are using. So please be aware that you may run into issues, that you will need to resolve yourself, if you use a different version of the libraries than what I am using.

Disclaimer

This is my first time using MSAL 2, I'm learning as I write this tutorial.

No Duplicates, We Only Want Singletons!

We need to ensure that our modules are only loaded once in our app. I learned of this special structure from one of Dan Wahlin's PluralSight courses - Angular Architecture and Best Practices

Create two new files in src/app/core

core.module.ts

 1import { NgModule, Optional, SkipSelf } from '@angular/core';
 2import { CommonModule } from '@angular/common';
 3import { EnsureModuleLoadedOnceGuard } from './ensureModuleLoadedOnceGuard';
 4
 5
 6
 7@NgModule({
 8  declarations: [],
 9  imports: [
10    CommonModule
11  ],
12  providers: []
13})
14export class CoreModule extends EnsureModuleLoadedOnceGuard {
15  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
16    super(parentModule);
17  }
18 }

and

ensureModuleLoadedOnceGaurd.ts

1// Thanks to Dan Wahlin for this structure
2export class EnsureModuleLoadedOnceGuard {
3  constructor(targetModule: any ) {
4    if (targetModule) {
5      throw new Error(`${targetModule.constructor.name} has already been loaded.
6      Import this module in the AppModule only.`);
7    }
8  }
9}

We now need to add the CoreModule to our app.module.ts

 1// .... Other imports omitted for brevity
 2
 3import { CoreModule } from './core/core.module';
 4
 5@NgModule({
 6  declarations: [
 7    AppComponent, SidenavComponent, ToolbarComponent, AlertsComponent, NotificationComponent
 8  ],
 9  imports: [
10    BrowserModule,
11    AppRoutingModule,
12    BrowserAnimationsModule,
13    MaterialModule,
14    FlexLayoutModule,
15    CoreModule,
16
17  // .... Other code omitted for brevity  

For more information, please take a look at Dan's PluralSight course.

Setup Authentication to Azure AD

Create a new file in the src directory called oauth.ts

Add the following to oauth.ts

 1export const OAuthSettings = {
 2  appId: '[YOUR APP ID FROM AZURE AD APP REGISTRATION]',
 3  redirectUri: 'http://localhost:4200',
 4  postLogoutRedirectUri: '/',
 5  spURL: '[YOUR SHAREPOINT URL]',
 6  scopes: [
 7    "user.read",
 8    "calendars.readwrite",
 9    "Sites.ReadWrite.All"
10  ]
11};

Now we need to ensure that this file is not accidentally added to our Github repository.

Open .gitignore

Add oauth.ts

1# creds
2oauth.ts
3
4// ... other code omitted for brevity

Save and close.

Now we need to add the Microsoft Graph and (MSAL) Microsoft Authentication Library.

Stop the app if it is running and then on the command line:

1npm i @azure/msal-angular @azure/msal-browser @microsoft/microsoft-graph-client @microsoft/microsoft-graph-types @pnp/graph @pnp/sp

Open app.module.ts

Add replace the contents with the following:

  1import { BrowserModule } from '@angular/platform-browser';
  2import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
  3import { NgModule } from '@angular/core';
  4
  5import { AppRoutingModule } from './app-routing.module';
  6import { AppComponent } from './app.component';
  7
  8import { MaterialModule } from './shared/material.module';
  9import { FlexLayoutModule } from '@angular/flex-layout';
 10import { SidenavComponent } from './core/sidenav/sidenav.component';
 11import { ToolbarComponent } from './core/toolbar/toolbar.component';
 12import { CoreModule } from './core/core.module';
 13import { OAuthSettings } from 'src/oauth';
 14
 15
 16// From Microsoft Sample - https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-v2-samples/angular10-sample-app/src/app/app.module.ts
 17import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
 18import { IPublicClientApplication, PublicClientApplication, InteractionType, BrowserCacheLocation, LogLevel } from '@azure/msal-browser';
 19import { MsalGuard, MsalInterceptor, MsalBroadcastService, MsalInterceptorConfiguration, MsalModule, MsalService, MSAL_GUARD_CONFIG, MSAL_INSTANCE, MSAL_INTERCEPTOR_CONFIG, MsalGuardConfiguration, MsalRedirectComponent } from '@azure/msal-angular';
 20
 21const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1;
 22
 23export function loggerCallback(logLevel: LogLevel, message: string) {
 24  console.log(message);
 25}
 26
 27export function MSALInstanceFactory(): IPublicClientApplication {
 28  return new PublicClientApplication({
 29    auth: {
 30      clientId: OAuthSettings.appId,
 31      redirectUri: OAuthSettings.redirectUri,
 32      postLogoutRedirectUri: OAuthSettings.postLogoutRedirectUri
 33    },
 34    cache: {
 35      cacheLocation: BrowserCacheLocation.LocalStorage,
 36      storeAuthStateInCookie: isIE, // set to true for IE 11
 37    },
 38    system: {
 39      loggerOptions: {
 40        loggerCallback,
 41        logLevel: LogLevel.Info,
 42        piiLoggingEnabled: false
 43      }
 44    }
 45  });
 46}
 47
 48export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
 49  const protectedResourceMap = new Map<string, Array<string>>();
 50  protectedResourceMap.set('https://graph.microsoft.com/v1.0/me', ['user.read']);
 51
 52  return {
 53    interactionType: InteractionType.Redirect,
 54    protectedResourceMap
 55  };
 56}
 57
 58export function MSALGuardConfigFactory(): MsalGuardConfiguration {
 59  return {
 60    interactionType: InteractionType.Redirect,
 61    authRequest: {
 62      scopes: ['user.read']
 63    }
 64  };
 65}
 66
 67
 68
 69@NgModule({
 70  declarations: [
 71    AppComponent,
 72    SidenavComponent,
 73    ToolbarComponent
 74  ],
 75  imports: [
 76    BrowserModule,
 77    BrowserAnimationsModule,
 78    AppRoutingModule,
 79    MaterialModule,
 80    FlexLayoutModule,
 81    CoreModule,
 82    MsalModule
 83  ],
 84  providers: [
 85    {
 86      provide: HTTP_INTERCEPTORS,
 87      useClass: MsalInterceptor,
 88      multi: true
 89    },
 90    {
 91      provide: MSAL_INSTANCE,
 92      useFactory: MSALInstanceFactory
 93    },
 94    {
 95      provide: MSAL_GUARD_CONFIG,
 96      useFactory: MSALGuardConfigFactory
 97    },
 98    {
 99      provide: MSAL_INTERCEPTOR_CONFIG,
100      useFactory: MSALInterceptorConfigFactory
101    },
102    MsalService,
103    MsalGuard,
104    MsalBroadcastService
105  ],
106  bootstrap: [AppComponent, MsalRedirectComponent]
107})
108export class AppModule { }

Before we setup authentication, you'll notice that our app's home page is blank. Let's create a dedicated home page, for now we will just show a welcome message.

Stop the app if it is running.

We complete the next part in two stages!

On the command line type

1ng g m home --routing -d

and you should see

1CREATE src/app/home/home-routing.module.ts (247 bytes)
2CREATE src/app/home/home.module.ts (272 bytes)

If everything looks good, remove the dryrun -d flag and execute the command once more.

Next type:

1ng g c home --style scss -d

and you should see

1CREATE src/app/home/home.component.scss (0 bytes)
2CREATE src/app/home/home.component.html (19 bytes)
3CREATE src/app/home/home.component.spec.ts (612 bytes)
4CREATE src/app/home/home.component.ts (268 bytes)
5UPDATE src/app/home/home.module.ts (335 bytes)

Notice that we will be updating the home.module.ts instead of app.module.ts

If everything looks good, remove the dryrun -d flag and execute the command once more.

Open home.component.html and change it's contents to:

1<p>Welcome! To Coffee Manager!</p>

Save and close home.component.html

Setup Authentication

Now we need to decide what areas of the app should require a login and which should remain open to all viewers.

For now let's just require that the Gift Cards page requires the user to login.

Stop the app if it is running.

We complete the next part in two stages!

First we create the module.

On the command line, type

1ng g m giftcards --routing -d 

You should see something similar to:

1CREATE src/app/giftcards/giftcards-routing.module.ts (252 bytes)
2CREATE src/app/giftcards/giftcards.module.ts (292 bytes)
3
4NOTE: The "dryRun" flag means no changes were made.

If everything looks good, remove the -d flag and execute the command once more.

Next we create the component.

1ng g c giftcards --style scss -d

and should see

1CREATE src/app/giftcards/giftcards.component.scss (0 bytes)
2CREATE src/app/giftcards/giftcards.component.html (24 bytes)
3CREATE src/app/giftcards/giftcards.component.spec.ts (647 bytes)
4CREATE src/app/giftcards/giftcards.component.ts (288 bytes)
5UPDATE src/app/giftcards/giftcards.module.ts (370 bytes)
6
7NOTE: The "dryRun" flag means no changes were made.

Notice that we will be updating the giftcards.module.ts instead of the app.module.ts

If everything looks good, remove the dryrun -d flag and execute the command once more.

Let's now add our route gaurd to giftcards-routing.module.ts

Open giftcards-routing.module.ts

Change the contents to

 1import { NgModule } from '@angular/core';
 2import { Routes, RouterModule } from '@angular/router';
 3import { MsalGuard } from '@azure/msal-angular';
 4
 5import { GiftcardsComponent } from './giftcards.component';
 6
 7const routes: Routes = [
 8  {path:'', component: GiftcardsComponent, canActivate : [MsalGuard] }
 9];
10
11@NgModule({
12  imports: [RouterModule.forChild(routes)],
13  exports: [RouterModule]
14})
15export class GiftcardsRoutingModule { 
16  static components = [GiftcardsComponent]
17}

Open giftcards.module.ts and change the contents to

 1import { NgModule } from '@angular/core';
 2import { CommonModule } from '@angular/common';
 3
 4import { GiftcardsRoutingModule } from './giftcards-routing.module';
 5
 6
 7@NgModule({
 8  declarations: [GiftcardsRoutingModule.components],
 9  imports: [
10    CommonModule,
11    GiftcardsRoutingModule
12  ]
13})
14export class GiftcardsModule { }

Let's add one more module, our About module

Stop the app if it is running and execute the following on the command line:

1ng g m about --routing -d 

You should see something similar to:

1CREATE src/app/about/about-routing.module.ts (252 bytes)
2CREATE src/app/about/about.module.ts (292 bytes)
3
4NOTE: The "dryRun" flag means no changes were made.

If everything looks good, remove the -d flag and execute the command once more.

Next we create the component.

1ng g c about --style scss -d

and you should see

1CREATE src/app/about/about.component.scss (0 bytes)
2CREATE src/app/about/about.component.html (24 bytes)
3CREATE src/app/about/about.component.spec.ts (647 bytes)
4CREATE src/app/about/about.component.ts (288 bytes)
5UPDATE src/app/about/about.module.ts (370 bytes)
6
7NOTE: The "dryRun" flag means no changes were made.

If everything looks good, remove the dryrun -d flag and execute the command once more.

We won't need to add a route gaurd to about-routing.module.ts, we want all users to be able to view this page without having to authenticate first.

But we do need to make some other changes such as making this a static component.

First! Open about.module.ts and make the following changes:

  • Remove the AboutComponent import statement
  • Import about-routing.module.ts
  • Change the @NgModule declarations array to
1import { AboutRoutingModule } from './about-routing.module';
2
3@NgModule({
4  declarations: [AboutRoutingModule.components],

Next open about-routing.module.ts and make the following changes:

  • Change the export class statement
1// ... other code omitted for brevity
2
3export class AboutRoutingModule {
4  static components = [ AboutComponent ]
5}

Next we need to add both our about.module.ts, home.module.ts and giftcards.module.ts to our app-routing.module.ts

Save and close all the files.

Open app-routing.module.ts

Change the contents to match the following:

 1import { NgModule } from '@angular/core';
 2import { Routes, RouterModule } from '@angular/router';
 3
 4const routes: Routes = [
 5  { path:'',pathMatch: 'full', loadChildren: () => import('./home/home.module').then(m => m.HomeModule)},
 6  { path: 'about', loadChildren: () => import('./about/about.module').then(m => m.AboutModule)},
 7  { path: 'giftcards', loadChildren: () => import('./giftcards/giftcards.module').then(m => m.GiftcardsModule)},
 8  { path: '**', pathMatch: 'full', loadChildren: () => import('./home/home.module').then(m => m.HomeModule)}
 9];
10
11@NgModule({
12  imports: [RouterModule.forRoot(routes, { useHash: false })],
13  exports: [RouterModule]
14})
15export class AppRoutingModule { }

Notice the { useHash: false } parameter

If this app were a SharePoint folder based app then we would want the # /demo/#/coffeemanager but for our app, I prefer the url without the #

Let's launch the app to see where we stand, on the command line type ng serve

If you click the About menu option you will see the message "About works!" but if you click Giftcards, it does nothing.

I installed MSAL 2 which does not support Implicit Flow as MSAL 1 did. I need to make some additional changes in my Azure portal app registration.

Go to your Azure Portal, Azure AD app registrations, select your app then select Authentication.

"Coffee Manager - Azure Portal Authentication settings"

Make sure both "Access tokens (used for implicit flows)" and "ID tokens (used for implicit and hybrid flows)" are not checked.

Click "This app has implicit grant settings enabled. If you are using any of these URIs in a SPA with MSAL.js 2.0, you should migrate URIs."

Check the box for your ReturnURI

Save

"Coffee Manager - MSAL Authentication Code Flow"

In the Implicit Grant and hybrid flows section

Uncheck both Access tokens and ID tokens.

Save

Launch the app once more

If you click the Giftcards button you should be presented with a Login prompt. If you recieve the error message "The reply url does not match" go back to your apps RedirectURI.

For instance I had made my RedirectURI http://localhost:4200/home when instead it should have been http://localhost:4200/ , remember the trailing whack or if you prefer the term slash.

You should now see the following

"Coffee Manager - Microsoft Login Screen using Code Flow"

If you click Sign-in-options

"Coffee Manager - Microsoft Login Screen using Code Flow - Sign In Options"

After signing in, now everytime a user opens the Giftcards screen, the app will check the Access Token, taken care of by MSAL for us, to ensure that the user is authenticated with Azure Active Directory.

This is great but it's not quite there yet, let's use the Microsoft Graph to display the current users DisplayName in the toolbar.

Start using Angular Material to display a Login/Logout button. We will also setup other Angular Material components such as a table, checkboxes, filters and more in the next post.

Next - Part: 5 - Authentication cont