Service for broadcasting MSAL events and tracking interaction status throughout the authentication lifecycle, enabling reactive programming patterns in Angular applications.
Service that provides Observable streams for MSAL events and interaction status, allowing applications to react to authentication state changes.
/**
* Service for broadcasting MSAL events and interaction status
* Provides reactive streams for authentication lifecycle events
*/
class MsalBroadcastService {
/** Observable stream of all MSAL events */
msalSubject$: Observable<EventMessage>;
/** Observable stream of interaction status changes */
inProgress$: Observable<InteractionStatus>;
/**
* Resets the interaction status to None
* Useful for clearing stuck interaction states
*/
resetInProgressEvent(): void;
}Configuration object for customizing broadcast behavior including event replay functionality.
/**
* Configuration object for MsalBroadcastService
* Controls event broadcasting and replay behavior
*/
interface MsalBroadcastConfiguration {
/** Number of events to replay when using ReplaySubject for late subscribers */
eventsToReplay: number;
}Usage Examples:
import { Component, OnInit, OnDestroy } from "@angular/core";
import { MsalBroadcastService } from "@azure/msal-angular";
import { EventMessage, EventType, InteractionStatus } from "@azure/msal-browser";
import { Subject } from "rxjs";
import { filter, takeUntil } from "rxjs/operators";
@Component({
selector: 'app-auth-status',
template: `
<div class="auth-status">
<div class="status-indicator" [class]="statusClass">
{{ statusMessage }}
</div>
<div *ngIf="lastEvent" class="last-event">
Last Event: {{ lastEvent.eventType }} at {{ lastEvent.timestamp }}
</div>
</div>
`
})
export class AuthStatusComponent implements OnInit, OnDestroy {
private readonly destroying$ = new Subject<void>();
statusMessage = 'Initializing...';
statusClass = 'initializing';
lastEvent: EventMessage | null = null;
constructor(private broadcastService: MsalBroadcastService) {}
ngOnInit() {
// Subscribe to interaction status changes
this.broadcastService.inProgress$
.pipe(takeUntil(this.destroying$))
.subscribe((status: InteractionStatus) => {
this.updateStatus(status);
});
// Subscribe to all MSAL events
this.broadcastService.msalSubject$
.pipe(takeUntil(this.destroying$))
.subscribe((payload: EventMessage) => {
this.lastEvent = payload;
this.handleMsalEvent(payload);
});
}
ngOnDestroy() {
this.destroying$.next(undefined);
this.destroying$.complete();
}
private updateStatus(status: InteractionStatus) {
switch (status) {
case InteractionStatus.None:
this.statusMessage = 'Ready';
this.statusClass = 'ready';
break;
case InteractionStatus.Login:
this.statusMessage = 'Logging in...';
this.statusClass = 'in-progress';
break;
case InteractionStatus.Logout:
this.statusMessage = 'Logging out...';
this.statusClass = 'in-progress';
break;
case InteractionStatus.AcquireToken:
this.statusMessage = 'Acquiring token...';
this.statusClass = 'in-progress';
break;
case InteractionStatus.SsoSilent:
this.statusMessage = 'Silent authentication...';
this.statusClass = 'in-progress';
break;
case InteractionStatus.HandleRedirect:
this.statusMessage = 'Handling redirect...';
this.statusClass = 'in-progress';
break;
}
}
private handleMsalEvent(payload: EventMessage) {
console.log('MSAL Event:', payload);
// Handle specific event types
switch (payload.eventType) {
case EventType.LOGIN_SUCCESS:
console.log('Login successful', payload.payload);
break;
case EventType.LOGIN_FAILURE:
console.error('Login failed', payload.error);
break;
case EventType.ACQUIRE_TOKEN_SUCCESS:
console.log('Token acquired', payload.payload);
break;
// Handle other event types as needed
}
}
}import { Component, OnInit } from "@angular/core";
import { MsalBroadcastService } from "@azure/msal-angular";
import { EventType, InteractionStatus } from "@azure/msal-browser";
import { filter } from "rxjs/operators";
@Component({
selector: 'app-login-monitor',
template: `
<div *ngIf="isLoggingIn" class="login-progress">
<mat-spinner diameter="20"></mat-spinner>
Authenticating...
</div>
`
})
export class LoginMonitorComponent implements OnInit {
isLoggingIn = false;
constructor(private broadcastService: MsalBroadcastService) {}
ngOnInit() {
// Monitor only login-related status changes
this.broadcastService.inProgress$
.pipe(
filter(status =>
status === InteractionStatus.Login ||
status === InteractionStatus.None
)
)
.subscribe(status => {
this.isLoggingIn = status === InteractionStatus.Login;
});
// Listen only to login success events
this.broadcastService.msalSubject$
.pipe(
filter(payload => payload.eventType === EventType.LOGIN_SUCCESS)
)
.subscribe(payload => {
console.log('User logged in successfully:', payload.payload);
// Perform post-login actions
this.onLoginSuccess();
});
// Listen only to login failure events
this.broadcastService.msalSubject$
.pipe(
filter(payload => payload.eventType === EventType.LOGIN_FAILURE)
)
.subscribe(payload => {
console.error('Login failed:', payload.error);
// Handle login failure
this.onLoginFailure(payload.error);
});
}
private onLoginSuccess() {
// Redirect to dashboard or refresh user data
// This could also trigger other Angular services
}
private onLoginFailure(error: any) {
// Show error message, redirect to error page, etc.
}
}import { Component, Injectable } from "@angular/core";
import { MsalBroadcastService } from "@azure/msal-angular";
import { EventType, InteractionStatus } from "@azure/msal-browser";
import { BehaviorSubject } from "rxjs";
import { filter, map } from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class TokenStatusService {
private tokenAcquisitionInProgress$ = new BehaviorSubject<boolean>(false);
constructor(private broadcastService: MsalBroadcastService) {
// Monitor token acquisition status
this.broadcastService.inProgress$
.pipe(
map(status => status === InteractionStatus.AcquireToken)
)
.subscribe(inProgress => {
this.tokenAcquisitionInProgress$.next(inProgress);
});
// Log token acquisition events
this.broadcastService.msalSubject$
.pipe(
filter(payload =>
payload.eventType === EventType.ACQUIRE_TOKEN_SUCCESS ||
payload.eventType === EventType.ACQUIRE_TOKEN_FAILURE
)
)
.subscribe(payload => {
if (payload.eventType === EventType.ACQUIRE_TOKEN_SUCCESS) {
console.log('Token acquired successfully:', payload.payload);
} else {
console.error('Token acquisition failed:', payload.error);
}
});
}
get isAcquiringToken$() {
return this.tokenAcquisitionInProgress$.asObservable();
}
}
@Component({
selector: 'app-api-button',
template: `
<button
(click)="callApi()"
[disabled]="isAcquiringToken$ | async"
class="api-button">
<mat-spinner *ngIf="isAcquiringToken$ | async" diameter="16"></mat-spinner>
{{ (isAcquiringToken$ | async) ? 'Getting Token...' : 'Call API' }}
</button>
`
})
export class ApiButtonComponent {
constructor(public tokenStatusService: TokenStatusService) {}
get isAcquiringToken$() {
return this.tokenStatusService.isAcquiringToken$;
}
callApi() {
// API call logic here
// Token acquisition will be handled by MsalInterceptor
}
}import { Component, OnInit } from "@angular/core";
import { MsalBroadcastService } from "@azure/msal-angular";
import { EventType, AccountInfo } from "@azure/msal-browser";
import { filter, map } from "rxjs/operators";
@Component({
selector: 'app-account-switcher',
template: `
<div class="account-info" *ngIf="currentAccount">
<img [src]="currentAccount.idTokenClaims?.picture"
[alt]="currentAccount.name"
class="profile-picture">
<span>{{ currentAccount.name }}</span>
<span class="username">{{ currentAccount.username }}</span>
</div>
<div *ngIf="accountChanged" class="account-changed-notice">
Account changed! Please refresh to see updated data.
<button (click)="refreshApp()">Refresh</button>
</div>
`
})
export class AccountSwitcherComponent implements OnInit {
currentAccount: AccountInfo | null = null;
accountChanged = false;
constructor(private broadcastService: MsalBroadcastService) {}
ngOnInit() {
// Monitor account changes
this.broadcastService.msalSubject$
.pipe(
filter(payload =>
payload.eventType === EventType.ACCOUNT_ADDED ||
payload.eventType === EventType.ACCOUNT_REMOVED
),
map(payload => payload.payload as AccountInfo)
)
.subscribe(account => {
console.log('Account change detected:', account);
if (this.currentAccount &&
this.currentAccount.homeAccountId !== account?.homeAccountId) {
this.accountChanged = true;
}
this.currentAccount = account;
});
// Monitor logout events
this.broadcastService.msalSubject$
.pipe(
filter(payload => payload.eventType === EventType.LOGOUT_SUCCESS)
)
.subscribe(() => {
this.currentAccount = null;
this.accountChanged = false;
});
}
refreshApp() {
window.location.reload();
}
}import { NgModule } from "@angular/core";
import {
MsalBroadcastService,
MsalBroadcastConfiguration,
MSAL_BROADCAST_CONFIG
} from "@azure/msal-angular";
// Configure broadcast service
const broadcastConfig: MsalBroadcastConfiguration = {
eventsToReplay: 1 // Replay the last event for late subscribers
};
@NgModule({
providers: [
{ provide: MSAL_BROADCAST_CONFIG, useValue: broadcastConfig },
MsalBroadcastService
]
})
export class AppModule { }import { Injectable } from "@angular/core";
import { MsalBroadcastService } from "@azure/msal-angular";
import { EventMessage, EventType } from "@azure/msal-browser";
import { Observable, merge } from "rxjs";
import { filter, share, startWith } from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class AuthEventService {
// Successful events stream
public readonly successEvents$: Observable<EventMessage>;
// Error events stream
public readonly errorEvents$: Observable<EventMessage>;
// Authentication state changes
public readonly authStateChanges$: Observable<EventMessage>;
constructor(private broadcastService: MsalBroadcastService) {
// Filter success events
this.successEvents$ = this.broadcastService.msalSubject$.pipe(
filter(payload =>
payload.eventType === EventType.LOGIN_SUCCESS ||
payload.eventType === EventType.ACQUIRE_TOKEN_SUCCESS ||
payload.eventType === EventType.SSO_SILENT_SUCCESS
),
share()
);
// Filter error events
this.errorEvents$ = this.broadcastService.msalSubject$.pipe(
filter(payload =>
payload.eventType === EventType.LOGIN_FAILURE ||
payload.eventType === EventType.ACQUIRE_TOKEN_FAILURE ||
payload.eventType === EventType.SSO_SILENT_FAILURE
),
share()
);
// Combine login/logout events
this.authStateChanges$ = this.broadcastService.msalSubject$.pipe(
filter(payload =>
payload.eventType === EventType.LOGIN_SUCCESS ||
payload.eventType === EventType.LOGOUT_SUCCESS
),
share()
);
}
}