Angular - Surface SharePoint List Data With MSGraph - Part 5 - Authentication cont.

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.

In my last post, I setup the app to login into Azure AD and return the token, heres where I messed up, I assumed MSAL 2, Authorization Code Flow with PKCE, would behave just like MSAL 1. I had to Google to find out what PKCE stands for, (Proof Key for Code Exchange)

NOPE! Should have read the documentation some more!

Repeat a thousand times. "Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs,Read The Docs"

I'm tired now, need more coffee!

Instead of returning the token silently, Azure AD returned the token in the URL. Now the navigation is incorrect, if you Inspect the Gift Cards page, you will see the error "The selector "app-redirect" did not match any elements blah blah blah". Yes that is the actual error blah, blah, blah.

No not really but sometimes it seems like it might as well be.Yes sometimes you have no choice, you must just read through it! But somedays the last thing I want to do is read through a bunch of jargon.

Anyways, the error is telling us the app has no idea where to send the viewer because there is no route for code=ksakfhkshjf98sdaf8usdfkw89y2rdfsnakldnf

Let's change this now.

Open app-routing.module.ts and add another route to the routes array.

1{ path: 'code',  loadChildren: () => import('./giftcards/giftcards.module').then(m => m.GiftcardsModule)},

Now if you navigate to the giftcards page, if you are not logged in, you are redirected to login, otherwise you will now see that the token is not present in the url and the page shows the message, giftcards works!

But what happens when we setup another page with a guard, code is set to match our giftcards page?

Let's find out! Let's create the Brands page.

Stop the app if it is running!

Type

1 ng g m brands  --routing -d 

You should see the following

1CREATE src/app/brands/brands-routing.module.ts (249 bytes)
2CREATE src/app/brands/brands.module.ts (280 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.

Now let's create the component.

1ng g c brands --style scss -d
1CREATE src/app/brands/brands.component.scss (0 bytes)
2CREATE src/app/brands/brands.component.html (21 bytes)
3CREATE src/app/brands/brands.component.spec.ts (626 bytes)
4CREATE src/app/brands/brands.component.ts (276 bytes)
5UPDATE src/app/brands/brands.module.ts (349 bytes)
6
7NOTE: The "dryRun" flag means no changes were made.

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

1CREATE src/app/brands/brands.component.scss (0 bytes)
2CREATE src/app/brands/brands.component.html (21 bytes)
3CREATE src/app/brands/brands.component.spec.ts (626 bytes)
4CREATE src/app/brands/brands.component.ts (276 bytes)
5UPDATE src/app/brands/brands.module.ts (349 bytes)

Time to set up the guard. Open brands-routing.module.ts and change the routes array to:

1import { MsalGuard } from '@azure/msal-angular';
2import { BrandsComponent } from './brands.component';
3
4const routes: Routes = [
5  {path:'', component: BrandsComponent, canActivate : [MsalGuard] }
6];

then change the export to:

1export class BrandsRoutingModule { 
2  static components = [ BrandsComponent ]
3}

Setup the route!

Open app-routing.module.ts and add

1{ path: 'brands', loadChildren: () => import('./brands/brands.module').then(m => m.BrandsModule)},

to the routes array.

Let's also build out our home page.

Open home-routing.module.ts and replace it's contents with:

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

Open home.module.ts and replace it's contents with:

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

Home Component!

Open home.component.html and replace it's contents with:

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

Save all the files. For this next part we need to ensure our previous session is not still active. We could shutdown our browser, or in an open browser delete the cache, or open another tab in incognito mode.

I prefer incognito/private mode because I usually have several tabs open at once and I don't want to have to log into my other sites again.

Open a Private or Incognito browser window and navigate to your apps url. Click the Brands menu button and you will be taken to the Microsoft login.

You may see that the return url also includes the token instead of just localhost:4200/brands

Time to read the docs again or let's take a look at one of Microsoft's sample code https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v2-samples/angular11-sample-app/src/app

Found an answer at https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/3042

Just open index.html and add an app-redirect below app-root

1<app-root></app-root>
2<app-redirect></app-redirect>

Now when the user logins, they will be taken back to the home page, then the window will redirect to the page that triggered the guard event.

Let's move on!

Time to create a Login / Logout service and also show the user a Login / Logout button in the toolbar.

Stop the app if it is running.

On the command line type:

1ng g s core/services/auth -d 

You should see

1CREATE src/app/core/services/auth.service.spec.ts (347 bytes)
2CREATE src/app/core/services/auth.service.ts (133 bytes)

If everything looks good, remove the -d flag and click enter one more time.

Auth Service!

Open auth.service.ts

Please Note! I copied the code from the MSAL 2.0 sample. https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-angular-v2-samples/angular11-sample-app/src/app app.component.ts

Copy the sample code in app.component.ts and paste it in our auth.service.ts.

We need to make some modifications to the copied code. Replace the contents with

 1import { Component, OnInit, Inject, OnDestroy, Injectable } from '@angular/core';
 2import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
 3import { AuthenticationResult, EventMessage, EventType, InteractionStatus, InteractionType, PopupRequest, RedirectRequest } from '@azure/msal-browser';
 4import { Subject } from 'rxjs';
 5import { filter, takeUntil } from 'rxjs/operators';
 6
 7@Injectable({
 8  providedIn: 'root',
 9})
10export class AuthService implements OnInit, OnDestroy {
11  isIframe = false;
12  loginDisplay = false;
13  userName = "";
14  private readonly _destroying$ = new Subject<void>();
15
16  constructor(
17    @Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
18    private authService: MsalService,
19    private msalBroadcastService: MsalBroadcastService
20  ) {}
21
22  ngOnInit(): void {
23    this.isIframe = window !== window.parent && !window.opener;
24
25    this.msalBroadcastService.inProgress$
26      .pipe(
27        filter((status: InteractionStatus) => status === InteractionStatus.None),
28        takeUntil(this._destroying$)
29      )
30      .subscribe(() => {
31        this.checkLogin();
32      });
33  }
34
35/**
36 * Check if the user is logged in. If true set the userName.
37 * @returns boolean
38 */
39  checkLogin() {
40    const curAccount = this.authService.instance.getAllAccounts();
41
42    if (curAccount === null ) {
43      console.log("No Accounts found!");
44      return false;
45    } else if ( curAccount .length > 1) {
46      console.log("More than one Account was found!");
47      return false;
48    } else if (curAccount.length === 1) {
49      this.userName = curAccount[0].name!;
50      return true;
51    }
52    return;
53  }
54
55  login() {
56    if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
57      if (this.msalGuardConfig.authRequest){
58        this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
59          .subscribe((response: AuthenticationResult) => {
60            this.authService.instance.setActiveAccount(response.account);
61          });
62        } else {
63          this.authService.loginPopup()
64            .subscribe((response: AuthenticationResult) => {
65              this.authService.instance.setActiveAccount(response.account);
66            });
67      }
68    } else {
69      if (this.msalGuardConfig.authRequest){
70        this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
71      } else {
72        this.authService.loginRedirect();
73      }
74    }
75  }
76
77  logout() {
78    this.authService.logout();
79  }
80
81  /**
82   * Return the current user name.
83   * @returns string
84   */
85  getCurrentLogin() {
86    return this.userName;
87  }
88
89  ngOnDestroy(): void {
90    this._destroying$.next(undefined);
91    this._destroying$.complete();
92  }
93
94}

Now before moving on to our model, we need to create notification.html and it's style sheet, notification.scss in the services directory.

notification.html

1<div>
2  {{data.type | titlecase}}
3</div>
4<div class="content-style">
5  {{data.message}}
6</div>

Save and close notification.html

notification.scss

1.content-style {
2  border: 1px solid white;
3  border-radius: 4px;
4  padding: 10px;
5  margin-top: 10px;
6  margin-bottom: 10px;
7}

Save and close notification.scss

Time to create our user model. Create a new folder in the core dir called models.

Create a new file in core/models named user.ts and add:

1
2export class User {
3    displayName: string;
4    email: string;
5    photo: any;
6    jobTitle: string;
7}

Save and close user.ts

Let's add a button to our toolbar to allow the user to Login / Logout of the app. We'll also use the Microsoft Graph to show the current login.

Open toolbar.component.html and add the following after the app title, and before the Light/Dark mode toggle button.

 1<span class="toolbar-spacer"></span>
 2
 3  <nav class="currentUser">
 4     <label>Welcome! {{ curLogin }}</label>
 5    
 6  </nav>
 7  &nbsp;
 8  <span>
 9    <button mat-flat-button *ngIf="!authenticated" (click)="signIn()" color="white">Login</button>
10    <button mat-flat-button *ngIf="authenticated" (click)="signOut()" color="white">Logout</button>
11  </span>

Save and close toolbar.component.html

Open toolbar.component.ts replace it's contents with:

 1import { Component, EventEmitter, OnInit, Output } from '@angular/core';
 2import { User } from '../models/user';
 3import { AuthService } from '../services/auth.service';
 4import { MsalBroadcastService} from '@azure/msal-angular';
 5import { EventMessage, EventType } from '@azure/msal-browser';
 6import { filter } from 'rxjs/operators';
 7
 8@Component({
 9selector: 'app-toolbar',
10templateUrl: './toolbar.component.html',
11styleUrls: ['./toolbar.component.scss']
12})
13export class ToolbarComponent implements OnInit {
14
15isAuthenticated = false;
16curLogin = "";
17
18@Output() toggleSidenav = new EventEmitter<void>();
19@Output() toggleTheme = new EventEmitter<void>();
20@Output() toggleDir = new EventEmitter<void>();
21
22
23constructor(private authService: AuthService, private msalBroadcastService: MsalBroadcastService) {}
24
25ngOnInit() {
26  this.msalBroadcastService.msalSubject$
27  .pipe(
28    filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
29  )
30  .subscribe((result: EventMessage) => {
31    console.log(result);
32    if (result?.payload?.account) {
33      this.authService;
34    }
35  });
36
37  this.checkAuth();
38}
39
40async signIn(): Promise<void> {
41  await this.authService.login();
42}
43
44signOut(): void {
45  this.authService.logout();
46}
47
48/**
49* Get the current logged in user name.
50*/
51checkAuth() {
52  let isAuthorized = this.authService.checkLogin();
53  if( isAuthorized ) {
54    this.curLogin = this.authService.getCurrentLogin();
55    this.isAuthenticated = true;
56  } else {
57    this.isAuthenticated = false;
58  }
59}
60
61}

Save and close all files.

Results!

Launch the app once more in an incognito window.

Click the Login button.

You should now see a popup window or the page containing the Microsoft Sign in. Sign in and you may see the Permission request dialog.

I'll leave it up to you whether or not you check the "Consent on behalf of your organization" and click Accept.

Since this is my tenant, I choose to check the box and click the Accept button.

Azure AD will return the viewer back to the Coffee Manager app and display the LogOut button and the name of the current user.

We are now in a good position to retrieve list data from SharePoint.

Next - Part: 6 - Use The Microsoft Graph To Surface SharePoint List Data (Coming Soon!)