Angular - Surface SharePoint List Data With MSGraph - Part 4 - Authentication
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.
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
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
If you click 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.