# MSAL Angular Debugging Implementation Guide ## Overview This document provides code updates to implement enhanced debugging for intermittent SSO login issues with Microsoft Office 365. These changes will help capture detailed logs even through redirects and provide better visibility into authentication failures. ## Problem Context Some users experience intermittent login failures requiring multiple clicks on the login button. Since our Web API only sees successful connections, the issue is occurring between the frontend and Microsoft's authentication service. ## Prerequisites - MSAL.js 2.x already implemented - Angular application with `@azure/msal-angular` and `@azure/msal-browser` --- ## 1. Enable Verbose MSAL Logging ### Update MSAL Configuration Modify your MSAL configuration file (typically `app.module.ts` or a dedicated config file): ```typescript import { LogLevel, Configuration } from '@azure/msal-browser'; export const msalConfig: Configuration = { auth: { clientId: 'YOUR_CLIENT_ID', authority: 'YOUR_AUTHORITY', redirectUri: 'YOUR_REDIRECT_URI', }, cache: { cacheLocation: 'sessionStorage', storeAuthStateInCookie: false, }, system: { loggerOptions: { loggerCallback: (level: LogLevel, message: string, containsPii: boolean) => { // Don't log PII (Personally Identifiable Information) if (containsPii) return; // Store logs in sessionStorage to persist through redirects try { const logs = JSON.parse(sessionStorage.getItem('msalDebugLogs') || '[]'); logs.push({ timestamp: new Date().toISOString(), level: LogLevel[level], message: message }); // Keep only last 100 logs to avoid storage issues if (logs.length > 100) { logs.shift(); } sessionStorage.setItem('msalDebugLogs', JSON.stringify(logs)); } catch (e) { console.error('Failed to store MSAL log:', e); } // Also log to console with appropriate level switch (level) { case LogLevel.Error: console.error(`[MSAL Error] ${message}`); break; case LogLevel.Warning: console.warn(`[MSAL Warning] ${message}`); break; case LogLevel.Info: console.info(`[MSAL Info] ${message}`); break; case LogLevel.Verbose: case LogLevel.Trace: console.log(`[MSAL ${LogLevel[level]}] ${message}`); break; } }, logLevel: LogLevel.Verbose, // Use LogLevel.Trace for even more details piiLoggingEnabled: false }, allowRedirectInIframe: false, windowHashTimeout: 60000, iframeHashTimeout: 6000, loadFrameTimeout: 6000 } }; ``` --- ## 2. Create a Logging Service Create a new service to centralize error logging: ```typescript // src/app/services/auth-debug.service.ts import { Injectable } from '@angular/core'; export interface AuthDebugLog { timestamp: string; type: 'error' | 'warning' | 'info' | 'event'; source: string; message: string; details?: any; } @Injectable({ providedIn: 'root' }) export class AuthDebugService { private readonly STORAGE_KEY = 'authDebugLogs'; private readonly MAX_LOGS = 200; logError(source: string, message: string, details?: any): void { this.addLog({ timestamp: new Date().toISOString(), type: 'error', source, message, details }); console.error(`[${source}] ${message}`, details); } logWarning(source: string, message: string, details?: any): void { this.addLog({ timestamp: new Date().toISOString(), type: 'warning', source, message, details }); console.warn(`[${source}] ${message}`, details); } logInfo(source: string, message: string, details?: any): void { this.addLog({ timestamp: new Date().toISOString(), type: 'info', source, message, details }); console.info(`[${source}] ${message}`, details); } logEvent(source: string, message: string, details?: any): void { this.addLog({ timestamp: new Date().toISOString(), type: 'event', source, message, details }); console.log(`[${source}] ${message}`, details); } private addLog(log: AuthDebugLog): void { try { const logs = this.getLogs(); logs.push(log); // Keep only most recent logs if (logs.length > this.MAX_LOGS) { logs.splice(0, logs.length - this.MAX_LOGS); } sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(logs)); } catch (e) { console.error('Failed to store auth debug log:', e); } } getLogs(): AuthDebugLog[] { try { const logs = sessionStorage.getItem(this.STORAGE_KEY); return logs ? JSON.parse(logs) : []; } catch (e) { console.error('Failed to retrieve auth debug logs:', e); return []; } } clearLogs(): void { sessionStorage.removeItem(this.STORAGE_KEY); sessionStorage.removeItem('msalDebugLogs'); } exportLogs(): string { const authLogs = this.getLogs(); const msalLogs = JSON.parse(sessionStorage.getItem('msalDebugLogs') || '[]'); return JSON.stringify({ exportDate: new Date().toISOString(), authLogs, msalLogs, userAgent: navigator.userAgent, cookies: this.getCookieStatus() }, null, 2); } private getCookieStatus(): any { return { cookiesEnabled: navigator.cookieEnabled, thirdPartyCookiesBlocked: this.detectThirdPartyCookieBlock() }; } private detectThirdPartyCookieBlock(): string { // Simple heuristic - not 100% accurate but gives indication const testCookie = 'test_3p_cookie=1'; document.cookie = testCookie + '; SameSite=None; Secure'; const blocked = !document.cookie.includes('test_3p_cookie'); document.cookie = testCookie + '; expires=Thu, 01 Jan 1970 00:00:00 UTC'; return blocked ? 'likely' : 'unlikely'; } } ``` --- ## 3. Monitor MSAL Events in App Component Update your main `AppComponent` to monitor MSAL events: ```typescript // src/app/app.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { MsalService, MsalBroadcastService, InteractionStatus } from '@azure/msal-angular'; import { EventMessage, EventType, InteractionType } from '@azure/msal-browser'; import { Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { AuthDebugService } from './services/auth-debug.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit, OnDestroy { private readonly _destroying$ = new Subject(); constructor( private msalService: MsalService, private msalBroadcastService: MsalBroadcastService, private authDebugService: AuthDebugService ) {} ngOnInit(): void { // Display stored logs from previous session (after redirect) this.displayStoredLogs(); // Monitor MSAL events this.monitorMsalEvents(); // Monitor interaction status this.monitorInteractionStatus(); } ngOnDestroy(): void { this._destroying$.next(); this._destroying$.complete(); } private displayStoredLogs(): void { const msalLogs = sessionStorage.getItem('msalDebugLogs'); if (msalLogs) { console.group('=== MSAL Logs from Previous Session ==='); try { const logs = JSON.parse(msalLogs); logs.forEach((log: any) => { console.log(`[${log.timestamp}] [${log.level}] ${log.message}`); }); } catch (e) { console.error('Failed to parse MSAL logs:', e); } console.groupEnd(); } const authLogs = this.authDebugService.getLogs(); if (authLogs.length > 0) { console.group('=== Auth Debug Logs from Previous Session ==='); authLogs.forEach(log => { console.log(`[${log.timestamp}] [${log.type}] [${log.source}] ${log.message}`, log.details); }); console.groupEnd(); } } private monitorMsalEvents(): void { this.msalBroadcastService.msalSubject$ .pipe( takeUntil(this._destroying$) ) .subscribe((event: EventMessage) => { this.authDebugService.logEvent('MSAL Event', event.eventType, { payload: event.payload, error: event.error, timestamp: event.timestamp }); // Handle specific events switch (event.eventType) { case EventType.LOGIN_START: this.authDebugService.logInfo('MSAL', 'Login started'); break; case EventType.LOGIN_SUCCESS: this.authDebugService.logInfo('MSAL', 'Login successful', event.payload); break; case EventType.LOGIN_FAILURE: this.authDebugService.logError('MSAL', 'Login failed', event.error); break; case EventType.ACQUIRE_TOKEN_START: this.authDebugService.logInfo('MSAL', 'Token acquisition started'); break; case EventType.ACQUIRE_TOKEN_SUCCESS: this.authDebugService.logInfo('MSAL', 'Token acquired successfully'); break; case EventType.ACQUIRE_TOKEN_FAILURE: this.authDebugService.logError('MSAL', 'Token acquisition failed', event.error); break; case EventType.SSO_SILENT_START: this.authDebugService.logInfo('MSAL', 'SSO silent authentication started'); break; case EventType.SSO_SILENT_SUCCESS: this.authDebugService.logInfo('MSAL', 'SSO silent authentication successful'); break; case EventType.SSO_SILENT_FAILURE: this.authDebugService.logError('MSAL', 'SSO silent authentication failed', event.error); break; } }); } private monitorInteractionStatus(): void { this.msalBroadcastService.inProgress$ .pipe( filter((status: InteractionStatus) => status === InteractionStatus.None), takeUntil(this._destroying$) ) .subscribe(() => { this.authDebugService.logEvent('MSAL', 'Interaction completed'); this.setActiveAccount(); }); } private setActiveAccount(): void { const accounts = this.msalService.instance.getAllAccounts(); if (accounts.length === 0) { this.authDebugService.logWarning('MSAL', 'No accounts found after interaction'); return; } if (accounts.length > 1) { this.authDebugService.logWarning('MSAL', `Multiple accounts found (${accounts.length})`); } const activeAccount = this.msalService.instance.getActiveAccount(); if (!activeAccount) { this.msalService.instance.setActiveAccount(accounts[0]); this.authDebugService.logInfo('MSAL', 'Active account set', { username: accounts[0].username }); } } } ``` --- ## 4. Enhanced Login Component Update your login component to capture more details: ```typescript // src/app/components/login/login.component.ts import { Component } from '@angular/core'; import { MsalService } from '@azure/msal-angular'; import { RedirectRequest } from '@azure/msal-browser'; import { AuthDebugService } from '../../services/auth-debug.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { isLoggingIn = false; constructor( private msalService: MsalService, private authDebugService: AuthDebugService ) {} login(): void { if (this.isLoggingIn) { this.authDebugService.logWarning('Login', 'Login already in progress, ignoring additional click'); return; } this.isLoggingIn = true; this.authDebugService.logInfo('Login', 'User initiated login', { timestamp: new Date().toISOString(), userAgent: navigator.userAgent, cookiesEnabled: navigator.cookieEnabled }); const loginRequest: RedirectRequest = { scopes: ['user.read'], // Add extra query parameters for debugging extraQueryParameters: { // Optional: add custom state for tracking debug: 'true' } }; this.msalService.loginRedirect(loginRequest) .subscribe({ next: () => { this.authDebugService.logInfo('Login', 'Login redirect initiated successfully'); }, error: (error) => { this.isLoggingIn = false; this.authDebugService.logError('Login', 'Login redirect failed', { errorCode: error.errorCode, errorMessage: error.errorMessage, subError: error.subError, stack: error.stack }); // Store error for analysis sessionStorage.setItem('lastLoginError', JSON.stringify({ errorCode: error.errorCode, errorMessage: error.errorMessage, timestamp: new Date().toISOString() })); }, complete: () => { this.authDebugService.logEvent('Login', 'Login redirect completed'); } }); } // Method to export logs for support exportDebugLogs(): void { const logs = this.authDebugService.exportLogs(); const blob = new Blob([logs], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `auth-debug-logs-${new Date().toISOString()}.json`; link.click(); window.URL.revokeObjectURL(url); } } ``` --- ## 5. Enhanced Auth Guard Update your authentication guard to log issues: ```typescript // src/app/guards/auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; import { MsalGuard } from '@azure/msal-angular'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { AuthDebugService } from '../services/auth-debug.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor( private msalGuard: MsalGuard, private authDebugService: AuthDebugService ) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable | Promise | boolean | UrlTree { this.authDebugService.logEvent('AuthGuard', 'Checking authentication', { url: state.url, timestamp: new Date().toISOString() }); return this.msalGuard.canActivate(route, state).pipe( tap((result) => { if (result === true) { this.authDebugService.logInfo('AuthGuard', 'Access granted', { url: state.url }); } else { this.authDebugService.logWarning('AuthGuard', 'Access denied, redirecting to login', { url: state.url, result }); } }) ); } } ``` --- ## 6. User Instructions for Browser DevTools ### For Chrome/Edge: 1. Open DevTools (F12) 2. Go to Console tab 3. **Check "Preserve log"** (important!) 4. Go to Network tab 5. **Check "Preserve log"** (important!) 6. Filter by "login.microsoftonline.com" ### For Firefox: 1. Open DevTools (F12) 2. Go to Console tab 3. Click Settings (gear icon) 4. **Check "Persist Logs"** (important!) 5. Go to Network tab (logs persist automatically) ### For Safari: 1. Enable Developer menu: Preferences > Advanced > Show Develop menu 2. Develop > Show Web Inspector 3. Console tab > **Check "Preserve Log"** --- ## 7. Testing the Implementation After deploying these changes: 1. **Reproduce the issue** with DevTools open (Preserve Log enabled) 2. **Check the console** for detailed MSAL logs 3. **Export logs** using the export button (if implemented in UI) 4. **Check sessionStorage** for: - `msalDebugLogs` - MSAL library logs - `authDebugLogs` - Application auth logs - `lastLoginError` - Last error details --- ## 8. What to Look For ### Common issues to identify: - **Third-party cookie blocking**: Look for messages about blocked cookies or CORS errors - **Token expiration**: Look for `ACQUIRE_TOKEN_FAILURE` events - **Silent auth failures**: Look for `SSO_SILENT_FAILURE` events - **Redirect loops**: Multiple `LOGIN_START` events without `LOGIN_SUCCESS` - **Network timeouts**: Failed requests to Microsoft endpoints ### Error codes to watch for: - `interaction_required` - User interaction needed (silent auth failed) - `AADB2C90077` - No existing session (cookie issue) - `login_required` - User must login again - `consent_required` - Additional consent needed --- ## 9. Collecting Information for Client Ask the client's Azure admin to check: 1. **Sign-in logs** in Azure Portal: - Navigate to: Azure AD > Monitoring & health > Sign-in logs - Filter by: Application name = [Your App Name] - Filter by: Status = Failure - Date range: Last 7-30 days 2. **For each failed attempt**, collect: - Error code (AADSTS...) - Correlation ID - Request ID - User agent - Date/time - Failure reason 3. **Use Microsoft's error lookup tool**: - URL: `https://login.microsoftonline.com/error?code=[ERROR_CODE]` --- ## 10. Rollback Plan If these changes cause any issues: 1. Remove the `system.loggerOptions` section from MSAL config 2. Remove or comment out the `AuthDebugService` injections 3. Keep the service file for future debugging needs --- ## Notes - These logs are stored in `sessionStorage` and will be cleared when the browser session ends - Logs contain **no PII** (Personally Identifiable Information) to comply with privacy requirements - The maximum number of stored logs is limited to prevent storage issues - Consider removing or disabling verbose logging in production after debugging is complete --- ## Support If you need assistance implementing these changes, please contact the backend team or create a ticket with the implementation questions.