DEV Community

Vikash Kumar
Vikash Kumar

Posted on • Originally published at Medium on

Complete Guide: Implementing Azure Entra ID SSO in Angular Standalone Applications

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Navigate to Azure Portal
  2. Sign in with your administrative account
  3. Search for “Azure Active Directory” or “Entra ID”
  4. Select Azure Active Directory from the results

Step 2: Register Your Application

  1. In the Azure AD overview, click App registrations in the left menu
  2. Click + New registration
  3. Fill out the registration form as below:
  4. Click Register
Name: MyAngularSSOApp
Supported account types: Accounts in this organizational directory only
Redirect URI:  
- Platform: Single-page application (SPA) 
- URI: http://localhost:4200
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Authentication Settings

After registration, you’ll be redirected to your app’s overview page:

  1. Note your Application (client) ID — you’ll need this later
  2. Note your Directory (tenant) ID — also required for configuration

Navigate to Authentication in the left menu:

  1. Under Platform configurations , verify your SPA redirect URI
  2. Add additional redirect URIs for different environments:
Development: http://localhost:4200 
Staging: https://your-staging-domain.com 
Production: https://your-production-domain.com
Enter fullscreen mode Exit fullscreen mode
  1. Under Implicit grant and hybrid flows , ensure:
  • Access tokens (for implicit flows)
  • ID tokens (for implicit and hybrid flows)
  1. 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:

  1. Go to API permissions
  2. Click + Add a permission
  3. Select Microsoft Graph
  4. Choose Delegated permissions
  5. Add permissions like:
  • User.Read (basic profile info)
  • email (email address)
  • profile (basic profile)
  1. 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"
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
};
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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 || [];
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
  }
];
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing and Troubleshooting

Step 1: Basic Testing

# Start development server
ng serve

# Open browser to http://localhost:4200
# Test authentication flow
Enter fullscreen mode Exit fullscreen mode

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:

  1. Verify redirect URI in Azure portal matches exactly
  2. Check for trailing slashes or case sensitivity
  3. 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:

  1. Ensure you’re using SPA (Single Page Application) configuration
  2. Verify MSAL configuration is correct
  3. 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('');
        })
      );
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Browser Developer Tools

  1. Network Tab: Check for failed requests to Microsoft endpoints
  2. Console Tab: Look for MSAL error messages
  3. 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
  }
};
Enter fullscreen mode Exit fullscreen mode

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)
  );
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]
  }
];
Enter fullscreen mode Exit fullscreen mode

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;
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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

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)