Skip to content

Commit

Permalink
Introduce MSAL for b2c in frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
BerendWouters committed Jan 30, 2024
1 parent d1a53c4 commit 32f94ca
Show file tree
Hide file tree
Showing 25 changed files with 9,963 additions and 5,204 deletions.
14,303 changes: 9,298 additions & 5,005 deletions Singer.API/ClientApp/package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions Singer.API/ClientApp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Singer",
"version": "0.1.0-issue-msal.1583",
"version": "0.1.0-issue-msal.1584",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down Expand Up @@ -29,6 +29,7 @@
"@auth0/angular-jwt": "^5.0.2",
"@azure/msal-angular": "^2.5.11",
"@azure/msal-browser": "^3.3.0",
"@codewithdan/observable-store": "^2.2.15",
"@danielmoncada/angular-datetime-picker": "^14.2.0",
"angular-calendar": "^0.28.2",
"date-fns": "^2.9.0",
Expand All @@ -54,9 +55,9 @@
"karma-coverage-istanbul-reporter": "~1.4.2",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2",
"typescript": "~4.6.4",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0"
"tslint": "~6.1.0",
"typescript": "~4.6.4"
}
}
18 changes: 13 additions & 5 deletions Singer.API/ClientApp/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { Routes, RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { MainComponent } from './main.component';
import { BrowserUtils } from '@azure/msal-browser';
import { MsalRedirectComponent } from '@azure/msal-angular';

const routes: Routes = [
{ path: 'login', loadChildren: () => import('./modules/login/login.module').then(m => m.LoginModule) },
{ path: 'login', loadChildren: () => import('./modules/login/login.module').then((m) => m.LoginModule) },
{
// Needed for handling redirect after login
path: 'auth',
loadChildren: () => import('./modules/login/login.module').then(m => m.LoginModule),
component: MsalRedirectComponent,
},
{
path: 'dashboard',
loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
loadChildren: () => import('./modules/dashboard/dashboard.module').then((m) => m.DashboardModule),
component: MainComponent,
},
{
path: 'voogden',
loadChildren: () => import('./modules/legalguardians/legalguardians.module').then(m => m.LegalguardiansModule),
loadChildren: () => import('./modules/legalguardians/legalguardians.module').then((m) => m.LegalguardiansModule),
component: MainComponent,
},
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
];

@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
imports: [
RouterModule.forRoot(routes, {
relativeLinkResolution: 'legacy', // Don't perform initial navigation in iframes or popups
initialNavigation: !BrowserUtils.isInIframe() && !BrowserUtils.isInPopup() ? 'enabledNonBlocking' : 'disabled', // Set to enabledBlocking to use Angular Universal
}),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
4 changes: 2 additions & 2 deletions Singer.API/ClientApp/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<app-nav-menu (logoutEvent)="onLogout()"></app-nav-menu>
<app-nav-menu (logoutEvent)="logout()"></app-nav-menu>
<div class="container">
<router-outlet></router-outlet>
<router-outlet *ngIf="!isIframe"></router-outlet>
</div>
162 changes: 153 additions & 9 deletions Singer.API/ClientApp/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,163 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';

import {
AuthenticationResult,
EventMessage,
EventType,
InteractionStatus,
InteractionType,
PopupRequest,
RedirectRequest,
} from '@azure/msal-browser';
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
import { b2cPolicyNames } from './modules/core/services/auth-config';
import { B2PCPolicyStore } from './modules/core/services/b2cpolicy.state.store';

interface Payload extends AuthenticationResult {
idTokenClaims: {
tfp?: string;
};
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'Singer';
export class AppComponent implements OnInit, OnDestroy {
title = 'Microsoft identity platform';
isIframe = false;
loginDisplay = false;
private readonly _destroying$ = new Subject<void>();

constructor(
@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
private authService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private b2cPolicyStateStore: B2PCPolicyStore
) {}

ngOnInit(): void {
this.isIframe = window !== window.parent && !window.opener;
this.setLoginDisplay();

this.authService.instance.enableAccountStorageEvents(); // Optional - This will enable ACCOUNT_ADDED and ACCOUNT_REMOVED events emitted when a user logs in or out of another tab or window
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None),
takeUntil(this._destroying$)
)
.subscribe(() => {
this.setLoginDisplay();
this.checkAndSetActiveAccount();
});

this.msalBroadcastService.msalSubject$
.pipe(
filter(
(msg: EventMessage) =>
msg.eventType === EventType.LOGIN_SUCCESS || msg.eventType === EventType.ACQUIRE_TOKEN_SUCCESS
),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
let payload: Payload = <AuthenticationResult>result.payload;

constructor(private router: Router) {}
/**
* For the purpose of setting an active account for UI update, we want to consider only the auth response resulting
* from SUSI flow. "tfp" claim in the id token tells us the policy (NOTE: legacy policies may use "acr" instead of "tfp").
* To learn more about B2C tokens, visit https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview
*/
if (payload.idTokenClaims['tfp'] === b2cPolicyNames.editProfile) {
window.alert('Profile has been updated successfully. \nPlease sign-in again.');
return this.logout();
}

return result;
});

this.msalBroadcastService.msalSubject$
.pipe(
filter(
(msg: EventMessage) =>
msg.eventType === EventType.LOGIN_FAILURE || msg.eventType === EventType.ACQUIRE_TOKEN_FAILURE
),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
// Add your auth error handling logic here
});
}

setLoginDisplay() {
this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
}

checkAndSetActiveAccount() {
/**
* If no active account set but there are accounts signed in, sets first account to active account
* To use active account set here, subscribe to inProgress$ first in your component
* Note: Basic usage demonstrated. Your app may require more complicated account selection logic
*/
let activeAccount = this.authService.instance.getActiveAccount();

if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
let accounts = this.authService.instance.getAllAccounts();
this.authService.instance.setActiveAccount(accounts[0]);
}
}

login(userFlowRequest?: RedirectRequest | PopupRequest) {
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
if (this.msalGuardConfig.authRequest) {
this.authService
.loginPopup({
...this.msalGuardConfig.authRequest,
...userFlowRequest,
} as PopupRequest)
.subscribe((response: AuthenticationResult) => {
this.authService.instance.setActiveAccount(response.account);
});
} else {
this.authService.loginPopup(userFlowRequest).subscribe((response: AuthenticationResult) => {
this.authService.instance.setActiveAccount(response.account);
});
}
} else {
if (this.msalGuardConfig.authRequest) {
this.authService.loginRedirect({
...this.msalGuardConfig.authRequest,
...userFlowRequest,
} as RedirectRequest);
} else {
this.authService.loginRedirect(userFlowRequest);
}
}
}

logout() {
if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
this.authService.logoutPopup({
mainWindowRedirectUri: '/',
});
} else {
this.authService.logoutRedirect();
}
}

editProfile() {
let editProfileFlowRequest: RedirectRequest | PopupRequest = {
authority: this.b2cPolicyStateStore.getPolicies().authorities.editProfile.authority,
scopes: [],
};

this.login(editProfileFlowRequest);
}

onLogout() {
this.router.navigateByUrl('/dashboard').then(() => {
// this.authService.logout();
});
// unsubscribe to events when component is destroyed
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
}
69 changes: 9 additions & 60 deletions Singer.API/ClientApp/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

// import { ApplicationInsightsModule, AppInsightsService } from '@markpieszak/ng-application-insights';
import { AppComponent } from './app.component';
Expand All @@ -13,20 +13,16 @@ import { NavMenuComponent } from './modules/core/components/nav-menu/nav-menu.co
import { NativeDateModule, MAT_DATE_LOCALE, DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { AdminModule } from './modules/admin/admin.module';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { ConfigurationService } from './modules/core/services/clientconfiguration.service';
import { ApplicationInsightsService } from './modules/core/services/applicationinsights.service';
import { registerLocaleData } from '@angular/common';
import localeBe from '@angular/common/locales/be';
import { MsalGuard, MsalInterceptor, MsalModule, MsalRedirectComponent } from '@azure/msal-angular';
import { InteractionType, PublicClientApplication } from '@azure/msal-browser';
import { msalConfig, protectedResources } from './modules/core/services/auth-config';
import { MsalGuardConfiguration, MsalRedirectComponent } from '@azure/msal-angular';
import { IPublicClientApplication, InteractionType, PublicClientApplication } from '@azure/msal-browser';
import { MSALStateStore } from './modules/core/services/msal.state.store';
import { MsalConfigDynamicModule } from './msal-config.dynamic.module';

// Import locale settings for Belgium
registerLocaleData(localeBe);

export function tokenGetter(): string {
return localStorage.getItem('token');
}
export const MY_FORMATS = {
parse: {
dateInput: 'D-MM-YYYY',
Expand All @@ -40,49 +36,16 @@ export const MY_FORMATS = {
@NgModule({
declarations: [AppComponent, MainComponent, NavMenuComponent],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
AdminModule,
HttpClientModule,
MaterialModule,
BrowserAnimationsModule,
NativeDateModule,
MsalModule.forRoot(
new PublicClientApplication(msalConfig),
{
// The routing guard configuration.
interactionType: InteractionType.Redirect,
authRequest: {
scopes: protectedResources.todoListApi.scopes,
},
},
{
// MSAL interceptor configuration.
// The protected resource mapping maps your web API with the corresponding app scopes. If your code needs to call another web API, add the URI mapping here.
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
[protectedResources.todoListApi.endpoint, protectedResources.todoListApi.scopes],
]),
}
),
// ApplicationInsightsModule.forRoot({
// instrumentationKeySetLater: true,
// }),
MsalConfigDynamicModule.forRoot(),
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true,
},
MsalGuard,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [ConfigurationService, ApplicationInsightsService],
multi: true,
},
BrowserAnimationsModule,
{ provide: MAT_DATE_LOCALE, useValue: 'nl-BE' },
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
{ provide: DateAdapter, useClass: MomentDateAdapter },
Expand All @@ -91,17 +54,3 @@ export const MY_FORMATS = {
bootstrap: [AppComponent, MsalRedirectComponent],
})
export class AppModule {}

export function initializeApp(
configurationService: ConfigurationService,
applicationInsightService: ApplicationInsightsService
) {
return () => {
configurationService
.load()
.toPromise()
.then(() => {
applicationInsightService.init();
});
};
}
2 changes: 1 addition & 1 deletion Singer.API/ClientApp/src/app/main.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./main.component.css'],
})
export class MainComponent implements OnInit {
constructor() {}
loginDisplay = false;

ngOnInit() {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
import { SingerRouterLink, singerRouterLinkRequirements } from '../../models/singer-routerlink.model';
import { Router } from '@angular/router';
import { MsalService } from '@azure/msal-angular';

@Component({
selector: 'app-nav-menu',
Expand Down Expand Up @@ -70,9 +71,11 @@ export class NavMenuComponent implements OnInit {

routerLinkRequirements: { [name: string]: boolean } = {};

constructor(public router: Router) {}
constructor(public router: Router, private authService: MsalService) {}

ngOnInit() {
this.isAuthenticated = this.authService.instance.getAllAccounts().length > 0;
this.updateRequirements();
// this.authService.isAdmin$.subscribe((res) => {
// this.isAdmin = res;
// this.updateRequirements();
Expand Down
Loading

0 comments on commit 32f94ca

Please sign in to comment.