Introduction
Single Sign-On (SSO) has become essential for modern web applications, providing users with seamless authentication experiences across multiple applications. Azure Entra ID (formerly Azure Active Directory) offers robust identity and access management capabilities that integrate perfectly with Angular applications.
This comprehensive guide walks you through implementing Azure Entra ID SSO in an Angular standalone application using Microsoft Authentication Library (MSAL). By the end of this tutorial, you’ll have a fully functional SSO implementation that provides secure, user-friendly authentication.
What You’ll Build:
- Secure Angular application with Azure Entra ID authentication
- Automatic login redirects and token management
- Protected routes and authentication guards
- Clean user interface with login/logout functionality
- Error handling and user feedback systems
Prerequisites
Before starting this implementation, ensure you have:
Technical Requirements
- Node.js (version 16 or higher)
- Angular CLI (version 15 or higher)
- Azure subscription with administrative access
- Basic knowledge of Angular, TypeScript, and Azure services
Development Environment
# Verify your setup
node --version
npm --version
ng version
Azure Permissions
- Azure Entra ID administrative access
- Ability to register applications in Azure portal
- Permission to configure app registrations
Azure Entra ID Configuration
Step 1: Access Azure Portal
- Navigate to Azure Portal
- Sign in with your administrative account
- Search for “Azure Active Directory” or “Entra ID”
- Select Azure Active Directory from the results
Step 2: Register Your Application
- In the Azure AD overview, click App registrations in the left menu
- Click + New registration
- Fill out the registration form as below:
- Click Register
Name: MyAngularSSOApp
Supported account types: Accounts in this organizational directory only
Redirect URI:
- Platform: Single-page application (SPA)
- URI: http://localhost:4200
Step 3: Configure Authentication Settings
After registration, you’ll be redirected to your app’s overview page:
- Note your Application (client) ID — you’ll need this later
- Note your Directory (tenant) ID — also required for configuration
Navigate to Authentication in the left menu:
- Under Platform configurations , verify your SPA redirect URI
- Add additional redirect URIs for different environments:
Development: http://localhost:4200
Staging: https://your-staging-domain.com
Production: https://your-production-domain.com
- Under Implicit grant and hybrid flows , ensure:
- ✅ Access tokens (for implicit flows)
- ✅ ID tokens (for implicit and hybrid flows)
- Under Advanced settings :
- ✅ Allow public client flows : Yes
- Supported account types : Verify correct setting
Step 4: Configure API Permissions (Optional)
If your application needs to access Microsoft Graph or other APIs:
- Go to API permissions
- Click + Add a permission
- Select Microsoft Graph
- Choose Delegated permissions
- Add permissions like:
- User.Read (basic profile info)
- email (email address)
- profile (basic profile)
- Click Grant admin consent if required
Step 5: Note Configuration Values
Save these values for your Angular configuration:
// You'll need these in your Angular app
const azureConfig = {
clientId: "your-application-client-id",
authority: "https://login.microsoftonline.com/your-tenant-id",
redirectUri: "http://localhost:4200"
};
Angular Project Setup
Step 1: Create New Angular Project
# Create a new Angular project
ng new angular-sso-app --routing --style=css --standalone
# Navigate to project directory
cd angular-sso-app
Step 2: Install MSAL Dependencies
# Install Microsoft Authentication Library for Angular
npm install @azure/msal-browser @azure/msal-angular
# Install additional dependencies for HTTP and routing
npm install @angular/common @angular/router @angular/animations
Step 3: Project Structure Setup
Create the following directory structure:
src/
├── app/
│ ├── components/
│ │ ├── login/
│ │ ├── home/
│ │ └── profile/
│ ├── guards/
│ ├── services/
│ └── config/
# Create components
ng generate component components/login
ng generate component components/home
ng generate component components/profile
# Create directories
mkdir -p src/app/guards
mkdir -p src/app/services
mkdir -p src/app/config
MSAL Implementation
Step 1: Create MSAL Configuration
Create src/app/config/msal.config.ts:
import { Configuration, BrowserCacheLocation } from '@azure/msal-browser';
export const msalConfig: Configuration = {
auth: {
clientId: 'your-application-client-id', // Replace with your client ID
authority: 'https://login.microsoftonline.com/your-tenant-id', // Replace with your tenant ID
redirectUri: 'http://localhost:4200',
postLogoutRedirectUri: 'http://localhost:4200'
},
cache: {
cacheLocation: BrowserCacheLocation.SessionStorage,
storeAuthStateInCookie: false
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) return;
console.log(`MSAL [${level}]: ${message}`);
},
piiLoggingEnabled: false
}
}
};
export const loginRequest = {
scopes: ['openid', 'profile', 'User.Read']
};
export const graphConfig = {
graphMeEndpoint: 'https://graph.microsoft.com/v1.0/me'
};
Step 2: Configure Main Application
Update src/main.ts:
import 'zone.js';
import { bootstrapApplication } from '@angular/platform-browser';
import { importProvidersFrom } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MsalModule,
MsalService,
MsalGuard,
MsalInterceptor,
MsalBroadcastService,
MsalRedirectComponent
} from '@azure/msal-angular';
import { PublicClientApplication, InteractionType } from '@azure/msal-browser';
import { AppComponent } from './app/app';
import { routes } from './app/app.routes';
import { msalConfig, loginRequest } from './app/config/msal.config';
const msalInstance = new PublicClientApplication(msalConfig);
// Initialize MSAL instance before bootstrapping the application
msalInstance.initialize().then(() => {
// Handle redirect promise to process any pending redirects
return msalInstance.handleRedirectPromise();
}).then(() => {
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
RouterModule.forRoot(routes),
HttpClientModule,
BrowserAnimationsModule,
MsalModule.forRoot(
msalInstance,
{
interactionType: InteractionType.Redirect,
authRequest: loginRequest
},
{
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
['https://graph.microsoft.com/v1.0/me', ['User.Read']]
])
}
)
),
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
MsalService,
MsalGuard,
MsalBroadcastService
]
}).catch(err => console.error(err));
}).catch(err => {
console.error('MSAL initialization failed:', err);
});
Step 3: Update App Component
Update src/app/app.ts:
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet, Router } from '@angular/router';
import { Subject, filter, takeUntil } from 'rxjs';
import {
MsalService,
MsalBroadcastService,
MSAL_GUARD_CONFIG,
MsalGuardConfiguration,
MsalRedirectComponent
} from '@azure/msal-angular';
import {
InteractionStatus,
PopupRequest,
RedirectRequest,
AuthenticationResult,
EventMessage,
EventType
} from '@azure/msal-browser';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<div class="app-container">
<main class="main-content">
<div *ngIf="isLoading" class="loading-spinner">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<router-outlet *ngIf="!isLoading"></router-outlet>
</main>
</div>
`,
styleUrls: ['./app.css']
})
export class AppComponent implements OnInit, OnDestroy {
title = 'Angular SSO App';
isLoggedIn = false;
userName = '';
isLoading = true;
private readonly _destroying$ = new Subject<void>();
constructor(
@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
private msalService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private router: Router
) {}
ngOnInit(): void {
this.initializeApp();
this.setLoginDisplay();
this.setupEventListeners();
// Fallback: Clear loading state after 5 seconds if MSAL doesn't respond
setTimeout(() => {
if (this.isLoading) {
console.log('Timeout reached, clearing loading state');
this.isLoading = false;
}
}, 5000);
}
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
private initializeApp(): void {
console.log('Initializing app...');
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => {
console.log('MSAL Interaction Status:', status);
return status === InteractionStatus.None;
}),
takeUntil(this._destroying$)
)
.subscribe(() => {
console.log('MSAL interaction completed, setting loading to false');
this.setLoginDisplay();
this.checkAndSetActiveAccount();
this.isLoading = false;
});
}
private setupEventListeners(): void {
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
console.log('Login successful');
this.checkAndSetActiveAccount();
});
}
private setLoginDisplay(): void {
this.isLoggedIn = this.msalService.instance.getAllAccounts().length > 0;
if (this.isLoggedIn) {
const account = this.msalService.instance.getAllAccounts()[0];
this.userName = account.name || account.username || 'User';
}
}
private checkAndSetActiveAccount(): void {
const accounts = this.msalService.instance.getAllAccounts();
if (accounts.length === 0) {
return;
}
if (accounts.length > 1) {
const currentAccounts = accounts.filter(account =>
account.homeAccountId.toUpperCase().includes('B2C_1_SUSI'.toUpperCase()) ||
account.homeAccountId.toUpperCase().includes('B2C_1_EDIT_PROFILE'.toUpperCase())
);
if (currentAccounts.length > 1) {
this.msalService.instance.setActiveAccount(currentAccounts[0]);
} else if (currentAccounts.length === 1) {
this.msalService.instance.setActiveAccount(currentAccounts[0]);
} else {
this.msalService.instance.setActiveAccount(accounts[0]);
}
} else {
this.msalService.instance.setActiveAccount(accounts[0]);
}
}
login(): void {
if (this.msalGuardConfig.authRequest) {
this.msalService.loginRedirect({
...this.msalGuardConfig.authRequest
} as RedirectRequest);
} else {
this.msalService.loginRedirect();
}
}
logout(): void {
this.msalService.logoutRedirect({
postLogoutRedirectUri: 'http://localhost:4200'
});
}
}
Authentication Service
Step 1: Create Authentication Service
Create src/app/services/auth.service.ts:
import { Injectable, Inject } from '@angular/core';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import {
MsalService,
MsalBroadcastService,
MSAL_GUARD_CONFIG,
MsalGuardConfiguration
} from '@azure/msal-angular';
import {
AccountInfo,
AuthenticationResult,
PopupRequest,
RedirectRequest,
SilentRequest,
InteractionStatus
} from '@azure/msal-browser';
import { graphConfig } from '../config/msal.config';
export interface UserProfile {
id: string;
name: string;
email: string;
jobTitle?: string;
department?: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private userProfileSubject = new BehaviorSubject<UserProfile | null>(null);
public userProfile$ = this.userProfileSubject.asObservable();
constructor(
@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
private msalService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private http: HttpClient
) {
this.initializeUser();
}
private initializeUser(): void {
const account = this.msalService.instance.getActiveAccount();
if (account) {
this.fetchUserProfile().subscribe();
}
}
/**
* Check if user is currently authenticated
*/
isAuthenticated(): boolean {
return this.msalService.instance.getAllAccounts().length > 0;
}
/**
* Get current user account information
*/
getCurrentUser(): AccountInfo | null {
return this.msalService.instance.getActiveAccount();
}
/**
* Check if an interaction is currently in progress
*/
private isInteractionInProgress(): boolean {
try {
// Try to get the interaction status from MSAL's internal state
const interactionStatus = this.msalService.instance.getActiveAccount();
// Check if there's an ongoing interaction by looking for interaction-related cache entries
return false; // For now, let the MSAL service handle the check
} catch (error) {
return false;
}
}
/**
* Perform login using redirect
*/
loginRedirect(): void {
try {
if (this.msalGuardConfig.authRequest) {
this.msalService.loginRedirect({
...this.msalGuardConfig.authRequest
} as RedirectRequest);
} else {
this.msalService.loginRedirect();
}
} catch (error: any) {
if (error.name === 'BrowserAuthError' && error.errorCode === 'interaction_in_progress') {
console.log('Login interaction already in progress, please wait...');
return;
}
console.error('Login failed:', error);
throw error;
}
}
/**
* Perform login using popup
*/
loginPopup(): Observable<AuthenticationResult> {
if (this.msalGuardConfig.authRequest) {
return this.msalService.loginPopup({
...this.msalGuardConfig.authRequest
} as PopupRequest);
} else {
return this.msalService.loginPopup();
}
}
/**
* Perform logout
*/
logout(): void {
this.userProfileSubject.next(null);
this.msalService.logoutRedirect({
postLogoutRedirectUri: 'http://localhost:4200'
});
}
/**
* Get access token silently
*/
getAccessToken(): Observable<string> {
const account = this.msalService.instance.getActiveAccount();
if (!account) {
return of('');
}
const request: SilentRequest = {
scopes: ['User.Read'],
account: account
};
return this.msalService.acquireTokenSilent(request).pipe(
map(result => result.accessToken),
catchError(error => {
console.error('Token acquisition failed:', error);
return of('');
})
);
}
/**
* Fetch user profile from Microsoft Graph
*/
fetchUserProfile(): Observable<UserProfile | null> {
if (!this.isAuthenticated()) {
return of(null);
}
return this.http.get(graphConfig.graphMeEndpoint).pipe(
map((profile: any) => {
const userProfile: UserProfile = {
id: profile.id,
name: profile.displayName || profile.name,
email: profile.mail || profile.userPrincipalName,
jobTitle: profile.jobTitle,
department: profile.department
};
this.userProfileSubject.next(userProfile);
return userProfile;
}),
catchError(error => {
console.error('Failed to fetch user profile:', error);
return of(null);
})
);
}
/**
* Check if user has specific roles or permissions
*/
hasRole(role: string): boolean {
const account = this.getCurrentUser();
if (!account || !account.idTokenClaims) {
return false;
}
// Implement role checking logic based on your token claims
const roles = (account.idTokenClaims as any)?.roles || [];
return roles.includes(role);
}
/**
* Get user's groups from token claims
*/
getUserGroups(): string[] {
const account = this.getCurrentUser();
if (!account || !account.idTokenClaims) {
return [];
}
return (account.idTokenClaims as any)?.groups || [];
}
}
Route Protection
Step 1: Create Authentication Guard
Create src/app/guards/auth.guard.ts:
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { MsalService, MsalBroadcastService } from '@azure/msal-angular';
import { InteractionStatus } from '@azure/msal-browser';
@Injectable({
providedIn: 'root'
})
export class AuthGuard {
constructor(
private msalService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private router: Router
) {}
canActivate(): Observable<boolean> | boolean {
return this.msalBroadcastService.inProgress$.pipe(
map((status: InteractionStatus) => {
if (status === InteractionStatus.None) {
const isAuthenticated = this.msalService.instance.getAllAccounts().length > 0;
if (!isAuthenticated) {
// Redirect to login
this.msalService.loginRedirect();
return false;
}
return true;
}
// Still processing authentication
return false;
}),
take(1)
);
}
canActivateChild(): Observable<boolean> | boolean {
return this.canActivate();
}
}
Step 2: Create Role-Based Guard
Create src/app/guards/role.guard.ts:
import { Injectable } from '@angular/core';
import { Router, ActivatedRouteSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleGuard {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | boolean {
const requiredRoles = route.data['roles'] as string[];
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
if (!this.authService.isAuthenticated()) {
this.router.navigate(['/login']);
return false;
}
const hasRequiredRole = requiredRoles.some(role =>
this.authService.hasRole(role)
);
if (!hasRequiredRole) {
this.router.navigate(['/unauthorized']);
return false;
}
return true;
}
}
Step 3: Configure Routes
Create src/app/app.routes.ts:
import { Routes } from '@angular/router';
import { MsalGuard } from '@azure/msal-angular';
import { Home } from './components/home/home';
import { Login } from './components/login/login';
import { Profile } from './components/profile/profile';
import { AuthGuard } from './guards/auth.guard';
import { RoleGuard } from './guards/role.guard';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
component: Home
},
{
path: 'login',
component: Login
},
{
path: 'profile',
component: Profile,
canActivate: [MsalGuard]
},
{
path: '**',
redirectTo: '/home'
}
];
User Interface Integration
Step 1: Create Home Component
Update src/app/components/home/home.ts:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { Subject, takeUntil, filter } from 'rxjs';
import { AuthService, UserProfile } from '../../services/auth.service';
import { MsalBroadcastService } from '@azure/msal-angular';
import { InteractionStatus, EventType, EventMessage } from '@azure/msal-browser';
@Component({
selector: 'app-home',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<div class="home-container">
<div class="hero-section">
<h1>Welcome to My SSO App</h1>
<p class="lead">Secure authentication with Azure Entra ID</p>
<div *ngIf="!isAuthenticated" class="auth-prompt">
<p>Please sign in to access your personalized dashboard</p>
<button
(click)="login()"
[disabled]="isLoggingIn"
class="btn btn-primary btn-lg">
<span *ngIf="!isLoggingIn">Sign In with Microsoft</span>
<span *ngIf="isLoggingIn">Signing In...</span>
</button>
</div>
<div *ngIf="isAuthenticated" class="welcome-user">
<h2>Hello, {{userProfile?.name}}!</h2>
<p>You are successfully authenticated</p>
<div class="user-actions">
<a routerLink="/profile" class="btn btn-outline-primary">
View Profile
</a>
<button (click)="logout()" class="btn btn-outline-secondary">
Sign Out
</button>
</div>
</div>
</div>
<div *ngIf="isAuthenticated" class="dashboard-section">
<h3>Quick Actions</h3>
<div class="action-cards">
<div class="card">
<h4>Profile</h4>
<p>View and manage your profile information</p>
<a routerLink="/profile" class="btn btn-primary">Go to Profile</a>
</div>
<div class="card">
<h4>Settings</h4>
<p>Configure your application preferences</p>
<a routerLink="/settings" class="btn btn-primary">Settings</a>
</div>
</div>
</div>
</div>
`,
styleUrls: ['./home.css']
})
export class Home implements OnInit, OnDestroy {
isAuthenticated = false;
isLoggingIn = false;
userProfile: UserProfile | null = null;
private readonly _destroying$ = new Subject<void>();
constructor(
private authService: AuthService,
private msalBroadcastService: MsalBroadcastService
) {}
ngOnInit(): void {
this.checkAuthentication();
this.setupEventListeners();
}
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
private checkAuthentication(): void {
this.isAuthenticated = this.authService.isAuthenticated();
if (this.isAuthenticated) {
this.authService.userProfile$.subscribe(profile => {
this.userProfile = profile;
});
// Fetch fresh profile data
this.authService.fetchUserProfile().subscribe();
}
}
private setupEventListeners(): void {
// Listen for login success events
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
console.log('Login successful');
this.isLoggingIn = false;
this.checkAuthentication();
});
// Listen for login failure events
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_FAILURE),
takeUntil(this._destroying$)
)
.subscribe((result: EventMessage) => {
console.log('Login failed');
this.isLoggingIn = false;
});
// Listen for interaction status changes
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None),
takeUntil(this._destroying$)
)
.subscribe(() => {
this.isLoggingIn = false;
this.checkAuthentication();
});
}
login(): void {
if (this.isLoggingIn) {
return; // Prevent multiple login attempts
}
this.isLoggingIn = true;
try {
this.authService.loginRedirect();
} catch (error) {
console.error('Login error:', error);
this.isLoggingIn = false;
}
}
logout(): void {
this.authService.logout();
}
}
Step 2: Create Profile Component
Create src/app/components/profile/profile.ts:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService, UserProfile } from '../../services/auth.service';
@Component({
selector: 'app-profile',
standalone: true,
imports: [CommonModule],
template: `
<div class="profile-container">
<div class="profile-header">
<h1>User Profile</h1>
<p>Your account information from Azure Entra ID</p>
</div>
<div *ngIf="loading" class="loading">
<div class="spinner"></div>
<p>Loading profile...</p>
</div>
<div *ngIf="userProfile && !loading" class="profile-content">
<div class="profile-card">
<div class="profile-avatar">
<div class="avatar-placeholder">
{{getInitials(userProfile.name)}}
</div>
</div>
<div class="profile-info">
<h2>{{userProfile.name}}</h2>
<p class="email">{{userProfile.email}}</p>
<div class="profile-details">
<div class="detail-row" *ngIf="userProfile.jobTitle">
<strong>Job Title:</strong>
<span>{{userProfile.jobTitle}}</span>
</div>
<div class="detail-row" *ngIf="userProfile.department">
<strong>Department:</strong>
<span>{{userProfile.department}}</span>
</div>
<div class="detail-row">
<strong>User ID:</strong>
<span>{{userProfile.id}}</span>
</div>
</div>
</div>
</div>
<div class="profile-actions">
<button (click)="refreshProfile()" class="btn btn-primary">
Refresh Profile
</button>
<button (click)="logout()" class="btn btn-outline-danger">
Sign Out
</button>
</div>
</div>
<div *ngIf="!userProfile && !loading" class="error-state">
<h3>Unable to load profile</h3>
<p>There was an error loading your profile information.</p>
<button (click)="refreshProfile()" class="btn btn-primary">
Try Again
</button>
</div>
</div>
`,
styleUrls: ['./profile.css']
})
export class Profile implements OnInit {
userProfile: UserProfile | null = null;
loading = true;
constructor(private authService: AuthService) {}
ngOnInit(): void {
this.loadProfile();
}
loadProfile(): void {
this.loading = true;
this.authService.userProfile$.subscribe(profile => {
this.userProfile = profile;
this.loading = false;
});
this.authService.fetchUserProfile().subscribe({
error: (error) => {
console.error('Failed to load profile:', error);
this.loading = false;
}
});
}
refreshProfile(): void {
this.loadProfile();
}
logout(): void {
this.authService.logout();
}
getInitials(name: string): string {
return name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase()
.substring(0, 2);
}
}
Step 3: Create Login Component
Create src/app/components/login/login.ts:
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule],
template: `
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1>Sign In</h1>
<p>Access your account with Microsoft SSO</p>
</div>
<div *ngIf="!isAuthenticated" class="login-content">
<div class="login-illustration">
<div class="microsoft-logo">
<svg width="48" height="48" viewBox="0 0 48 48">
<path fill="#f25022" d="M0 0h23v23H0z"/>
<path fill="#00a4ef" d="M25 0h23v23H25z"/>
<path fill="#7fba00" d="M0 25h23v23H0z"/>
<path fill="#ffb900" d="M25 25h23v23H25z"/>
</svg>
</div>
</div>
<div class="login-form">
<button
(click)="login()"
class="btn btn-microsoft"
[disabled]="isLoading">
<span *ngIf="!isLoading">Sign in with Microsoft</span>
<span *ngIf="isLoading">Signing in...</span>
</button>
<div class="login-benefits">
<h3>Benefits of signing in:</h3>
<ul>
<li>✓ Secure authentication</li>
<li>✓ Single sign-on across apps</li>
<li>✓ Access to personalized features</li>
<li>✓ Automatic session management</li>
</ul>
</div>
</div>
</div>
<div *ngIf="isAuthenticated" class="already-signed-in">
<div class="success-icon">✓</div>
<h2>You're already signed in!</h2>
<p>Redirecting to your dashboard...</p>
</div>
</div>
</div>
`,
styleUrls: ['./login.css']
})
export class Login implements OnInit {
isAuthenticated = false;
isLoading = false;
constructor(
private authService: AuthService,
private router: Router
) {}
ngOnInit(): void {
this.isAuthenticated = this.authService.isAuthenticated();
if (this.isAuthenticated) {
// Redirect authenticated users to home
setTimeout(() => {
this.router.navigate(['/home']);
}, 2000);
}
}
login(): void {
this.isLoading = true;
this.authService.loginRedirect();
}
}
Testing and Troubleshooting
Step 1: Basic Testing
# Start development server
ng serve
# Open browser to http://localhost:4200
# Test authentication flow
Step 2: Common Issues and Solutions
Issue 1: Redirect URI Mismatch
Error: AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application
Solution:
- Verify redirect URI in Azure portal matches exactly
- Check for trailing slashes or case sensitivity
- Ensure SPA platform is selected (not Web)
Issue 3: CORS Issues
Error: Access to fetch at 'https://login.microsoftonline.com/...' from origin 'http://localhost:4200' has been blocked by CORS policy
Solution:
- Ensure you’re using SPA (Single Page Application) configuration
- Verify MSAL configuration is correct
- Check browser developer tools for specific error details
Issue 3: Token Issues
Error: InteractionRequiredAuthError or Token acquisition failed
Solution:
// Add error handling in your service
getAccessToken(): Observable<string> {
const account = this.msalService.instance.getActiveAccount();
if (!account) {
return of('');
}
const request: SilentRequest = {
scopes: ['User.Read'],
account: account
};
return this.msalService.acquireTokenSilent(request).pipe(
map(result => result.accessToken),
catchError(error => {
console.error('Silent token acquisition failed:', error);
// Try interactive token acquisition
return this.msalService.acquireTokenPopup(request).pipe(
map(result => result.accessToken),
catchError(popupError => {
console.error('Popup token acquisition failed:', popupError);
return of('');
})
);
})
);
}
Step 3: Debugging Tools
Enable MSAL Logging
// In msal.config.ts
export const msalConfig: Configuration = {
// ... other config
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) return;
console.log(`MSAL [${level}]: ${message}`);
},
piiLoggingEnabled: false,
logLevel: LogLevel.Verbose // Enable verbose logging
}
}
};
Browser Developer Tools
- Network Tab: Check for failed requests to Microsoft endpoints
- Console Tab: Look for MSAL error messages
- Application Tab: Inspect stored tokens in Session/Local Storage
Step 4: Testing Checklist
- [] Login redirect works correctly
- [] User can access protected routes after authentication
- [] Logout clears session and redirects properly
- [] Token refresh works automatically
- [] Profile information displays correctly
- [] Error handling works for network issues
- [] Authentication guards protect routes properly
Best Practices
Security Best Practices
1. Token Storage
// Use sessionStorage for better security
export const msalConfig: Configuration = {
cache: {
cacheLocation: BrowserCacheLocation.SessionStorage, // More secure than localStorage
storeAuthStateInCookie: false // Set to true only if needed for IE11
}
};
2. Scope Management
// Request minimal scopes needed
export const loginRequest = {
scopes: ['openid', 'profile', 'User.Read'] // Only what you need
};
// Request additional scopes when needed
private requestAdditionalScopes(scopes: string[]): Observable<string> {
const account = this.msalService.instance.getActiveAccount();
const request = {
scopes: scopes,
account: account
};
return this.msalService.acquireTokenSilent(request).pipe(
map(result => result.accessToken)
);
}
3. Error Handling
// Comprehensive error handling
private handleAuthError(error: any): void {
if (error.errorCode === 'user_cancelled') {
console.log('User cancelled authentication');
return;
}
if (error.errorCode === 'consent_required') {
// Handle consent required
this.msalService.acquireTokenPopup(this.loginRequest);
return;
}
console.error('Authentication error:', error);
// Show user-friendly error message
}
Performance Best Practices
1. Lazy Loading
// Use lazy loading for non-critical routes
const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
canActivate: [MsalGuard]
}
];
2. Caching Strategy
// Cache user profile to avoid repeated API calls
private userProfileCache: UserProfile | null = null;
private profileCacheExpiry: number = 0;
fetchUserProfile(): Observable<UserProfile | null> {
const now = Date.now();
// Return cached profile if still valid (5 minutes)
if (this.userProfileCache && now < this.profileCacheExpiry) {
return of(this.userProfileCache);
}
return this.http.get(graphConfig.graphMeEndpoint).pipe(
map((profile: any) => {
this.userProfileCache = this.mapProfile(profile);
this.profileCacheExpiry = now + (5 * 60 * 1000); // 5 minutes
return this.userProfileCache;
})
);
}
Accessibility Best Practices
1. ARIA Labels
<!-- Add proper ARIA labels for authentication states -->
<button
(click)="login()"
[attr.aria-label]="isLoading ? 'Signing in, please wait' : 'Sign in with Microsoft'"
[disabled]="isLoading">
Sign In
</button>
2. Loading States
<!-- Provide clear loading feedback -->
<div *ngIf="isLoading"
role="status"
aria-live="polite"
class="loading-spinner">
<span class="sr-only">Loading authentication...</span>
</div>
Monitoring and Analytics
1. Authentication Events
// Track authentication events
private trackAuthEvent(event: string, data?: any): void {
// Send to your analytics service
console.log('Auth Event:', event, data);
// Example with Application Insights
// this.applicationInsights.trackEvent(event, data);
}
// Use in authentication flows
login(): void {
this.trackAuthEvent('login_attempt');
this.msalService.loginRedirect();
}
2. Error Tracking
// Track authentication errors
private trackAuthError(error: any): void {
const errorData = {
errorCode: error.errorCode,
errorMessage: error.message,
timestamp: new Date().toISOString()
};
// Send to error tracking service
console.error('Auth Error:', errorData);
}
Conclusion
You’ve successfully implemented Azure Entra ID SSO in your Angular standalone application! This comprehensive implementation provides:
What You’ve Accomplished
✅ Secure Authentication: Users can sign in with their Microsoft credentials
✅ Route Protection: Sensitive areas are protected by authentication guards ✅ Token Management: Automatic token refresh and secure storage
✅ User Profile Integration: Access to user information from Microsoft Graph
✅ Error Handling: Comprehensive error handling and user feedback
✅ Best Practices: Security, performance, and accessibility considerations
Next Steps
1. Production Deployment:
- Update redirect URIs for production environment
- Configure proper HTTPS certificates
- Set up monitoring and logging
2. Enhanced Features:
- Implement role-based access control
- Add multi-tenant support if needed
- Integrate with Microsoft Graph APIs
3. Testing:
- Add unit tests for authentication service
- Implement e2e tests for authentication flows
- Test with different user scenarios
Additional Resources
- Microsoft Authentication Library (MSAL) Documentation
- Azure Entra ID Documentation
- Microsoft Graph API Documentation
- Angular Security Best Practices
Support and Community
Happy coding! 🚀
This guide provides a solid foundation for implementing Azure Entra ID SSO in Angular applications. Remember to keep your dependencies updated and follow Microsoft’s latest security recommendations.

Top comments (0)