AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.64k stars 2.65k forks source link

Safari Browser which throws this error BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API. #5668

Closed abhisheknpj closed 1 year ago

abhisheknpj commented 1 year ago

Core Library

MSAL.js v2 (@azure/msal-browser)

Core Library Version

2.32.2

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

2.5.2

Public or Confidential Client?

Public

Description

I am seeing this error right after login in Safari browser. I have disabled cookie blocks and disabled prevent cross site tracking in my safari browser but I am still not able to login using safari. It is working fine on all other browsers. Any help with this is greatly appreciated.

MSAL Configuration

**
 * This file contains authentication parameters. Contents of this file
 * is roughly the same across other MSAL.js libraries. These parameters
 * are used to initialize Angular and MSAL Angular configurations in
 * in app.module.ts file.
 */

import { LogLevel, Configuration, BrowserCacheLocation } from '@azure/msal-browser';
import { applicationConfig } from './app.config';
import { ClientConfig } from './client-config';

declare const window: Window;
const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1;
const isSafari = ((navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1));
let currentConfig: ClientConfig;
currentConfig = getClientConfig()
function getClientConfig() : ClientConfig {
        switch (window.location.hostname) {
          case getHostName(applicationConfig.production.clientUrl):
            currentConfig = applicationConfig.production;
            break;
          case getHostName(applicationConfig.uat.clientUrl):
            currentConfig = applicationConfig.uat;
            break;
          case getHostName(applicationConfig.qa.clientUrl):
            currentConfig = applicationConfig.qa;
            break;
          case getHostName(applicationConfig.staging.clientUrl):
            currentConfig = applicationConfig.staging;
            break;
          default:
            currentConfig = applicationConfig.local;
        }
        return currentConfig;
}

    function getHostName(url: string): string {
    return url.replace('https://', '').replace('/', '');
  }

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL.js configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
 */
export const msalConfig: Configuration = {
    auth: {
        clientId: currentConfig.clientId, // This is the ONLY mandatory field that you need to supply.
        authority: 'https://login.microsoftonline.com/mytenantid', // Defaults to "https://login.microsoftonline.com/common"
        redirectUri: currentConfig.redirectUri, //'https://localhost:4200', // Points to window.location.origin by default. You must register this URI on Azure portal/App Registration.
        postLogoutRedirectUri: currentConfig.postLogoutRedirectUri,//'https://localhost:4200', // Points to window.location.origin by default.
        clientCapabilities: ['CP1'],// This lets the resource server know that this client can handle claim challenges.
    },
    cache: {
        cacheLocation: BrowserCacheLocation.LocalStorage, // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
        storeAuthStateInCookie: isIE || isSafari, // Set this to "true" if you are having issues on IE11 or Edge. Remove this line to use Angular Universal
    },
    system: {
        /**
         * Below you can configure MSAL.js logs. For more information, visit:
         * https://docs.microsoft.com/azure/active-directory/develop/msal-logging-js
         */
        loggerOptions: {
            loggerCallback(logLevel: LogLevel, message: string) {
                console.log(message);
            },
            logLevel: LogLevel.Trace,
            piiLoggingEnabled: true
        }
    }
}

/**
 * Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
 */
export const protectedResources = {
    backendAPI: {
        endpoint: currentConfig.apiUri,  
        scopes: {
            role:[currentConfig.roleScope]]
        }
    },
    msgraphApi: {
        endpoint: "https://graph.microsoft.com/v1.0",
        scopes: {
            UserRead:["User.Read"] 
        }
    }
}

/**
 * Scopes you add here will be prompted for user consent during sign-in.
 * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
 * For more information about OIDC scopes, visit:
 * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
 */
export const loginRequest = {
    scopes: ['openid', 'profile', 'email',  'User.Read' ]
};

Relevant Code Snippets

import { BreakpointObserver } from '@angular/cdk/layout';
import { Component, OnInit, Inject, OnDestroy, ViewChild, AfterViewInit, OnChanges, ElementRef, HostBinding } from '@angular/core';
import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
import { AuthenticationResult, EventMessage, EventType, InteractionStatus, InteractionType, PopupRequest, RedirectRequest } from '@azure/msal-browser';
import { UserLoginStatusService } from './services/user-login-status.service';
import { Subject, Subscription } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { APIService } from './services/api.service';
import { Profile } from './models/profile';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { MatSidenav } from '@angular/material/sidenav';
import { FormControl } from '@angular/forms';
import { OverlayContainer } from '@angular/cdk/overlay';
import { version } from './utils/version';
import { PreventBackService } from './services/prevent-back.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
    title = 'Faculty Management System';
    isIframe = false;
    loginDisplay = false;
    sub$: Subscription[] = [];
    private readonly _destroying$ = new Subject<void>();
    profile?: Profile;
    profilePicture?: SafeUrl;
    year: number = new Date().getFullYear();
    version = version.buildNumber;

    //Used by dark mode toggle
    @HostBinding('class') className = '';
    toggleControl = new FormControl(false);

    @ViewChild(MatSidenav)
    sideNav!: MatSidenav;

    constructor(private observer: BreakpointObserver,
        @Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration,
        private authService: MsalService,
        private msalBroadcastService: MsalBroadcastService,
        private userLoginStatusService: UserLoginStatusService,
        private apiService: APIService,
        private domSanitizer: DomSanitizer,
        private overlay: OverlayContainer,
        private preventBackService: PreventBackService
    ) {
    }

    ngOnInit(): void {
        this.preventBackService.preventBackButton();
        //First check on initail load if dark mode is enabled or when page is refreshed
        this.toggleControl.setValue(localStorage.getItem('darkMode') === 'true' ? true : false);
        this.className = localStorage.getItem('darkMode') === 'true' ? 'darkMode' : '';

        if (this.className === 'darkMode') {
            this.overlay.getContainerElement().classList.add('darkMode');
        }
        else {
            this.overlay.getContainerElement().classList.remove('darkMode');
        }

        //Check if dark mode is enabled or disabled based on toggle
        this.toggleControl.valueChanges.subscribe((darkMode) => {
            const darkClassName = 'darkMode';
            this.className = darkMode ? darkClassName : '';
            localStorage.setItem('darkMode', darkMode ? 'true' : 'false');
            if (darkMode) {
                this.overlay.getContainerElement().classList.add(darkClassName);
            } else {
                this.overlay.getContainerElement().classList.remove(darkClassName);
            }

        });

        this.isIframe = window !== window.parent && !window.opener;
        this.setLoginDisplay();

        this.authService.instance.enableAccountStorageEvents(); // Optional - This will enable ACCOUNT_ADDED and ACCOUNT_REMOVED events emitted when a user logs in or out of another tab or window
        /**
         * You can subscribe to MSAL events as shown below. For more info,
         * visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/events.md
         */

        this.msalBroadcastService.msalSubject$.pipe(
            filter((msg: EventMessage) => msg.eventType === EventType.ACCOUNT_ADDED || msg.eventType === EventType.ACCOUNT_REMOVED),
            takeUntil(this._destroying$),
        ).subscribe((result: EventMessage) => {
            if (this.authService.instance.getAllAccounts().length === 0) {
                window.location.pathname = "/";
            } else {
                this.setLoginDisplay();
            }
        });

        this.msalBroadcastService.inProgress$
            .pipe(
                filter((status: InteractionStatus) => status === InteractionStatus.None),
                takeUntil(this._destroying$)
            )
            .subscribe(() => {
                this.setLoginDisplay();
                this.checkAndSetActiveAccount();
                //this.callbackEnd();

            });
    }

    // callbackEnd() {
    //     this.authService.handleRedirectObservable().subscribe({
    //         next: (result: AuthenticationResult) => {
    //             if (result?.accessToken != null) {
    //                 //only call this endpoint if the user is logged in for the first time.
    //                 this.sub$.push(this.userLoginStatusService.getUserInfo().subscribe(() => { }));
    //             }

    //         },
    //         error: (error) => console.log(error)
    //     });
    // }

    setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
        this.userLoginStatusService.isUserLoggedIn.next(this.loginDisplay);
        if (this.authService.instance.getAllAccounts().length > 0) {
            this.getProfile();
            this.getProfilePicture();
        }
    }

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

        if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
            let accounts = this.authService.instance.getAllAccounts();
            // add your code for handling multiple accounts here
            this.authService.instance.setActiveAccount(accounts[0]);
            this.getProfile();
            this.getProfilePicture();
        }
    }

    login() {

        if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
            if (this.msalGuardConfig.authRequest) {
                this.authService.loginPopup({ ...this.msalGuardConfig.authRequest } as PopupRequest)
                    .subscribe((response: AuthenticationResult) => {
                        this.authService.instance.setActiveAccount(response.account);
                    });
            } else {
                this.authService.loginPopup()
                    .subscribe((response: AuthenticationResult) => {
                        this.authService.instance.setActiveAccount(response.account);
                    });
            }
        } else {
            this.msalBroadcastService.inProgress$
                .pipe(
                    filter((status: InteractionStatus) => status === InteractionStatus.None),
                )
                .subscribe(() => {
                    if (this.msalGuardConfig.authRequest) {
                        this.authService.loginRedirect({ ...this.msalGuardConfig.authRequest } as RedirectRequest);
                    } else {
                        this.authService.loginRedirect();
                    }
                });
        }
    }

    logout() {
        const activeAccount = this.authService.instance.getActiveAccount() || this.authService.instance.getAllAccounts()[0];

        if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
            this.authService.logoutPopup({
                account: activeAccount,
            });
        } else {
            this.authService.logoutRedirect({
                account: activeAccount,
            });
        }
        sessionStorage.clear();
        localStorage.clear();
    }

    // unsubscribe to events when component is destroyed
    ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
        this.sub$.map(x => x != null ? x.unsubscribe() : null);
    }

    //Role related methods

    hasRole(role: string): boolean {
        return this.apiService.hasRole(role);
    }

    ngAfterViewInit(): void {
        //Called after ngAfterContentInit when the component's view has been initialized. Applies to components only.
        setTimeout(() => {
            this.observer.observe(['(max-width: 800px)']).subscribe((result: any) => {
                if (result.matches) {
                    this.sideNav.mode = 'over';
                    this.sideNav.close();
                }
                else {
                    this.sideNav.mode = 'side';
                    this.sideNav.open();
                }
            })
        }, 0);
    }

    getProfile() {
        let profile = localStorage.getItem('profile');
        if (profile != null || profile != undefined) {
            this.profile = JSON.parse(profile);
        }
        else {
            this.sub$.push(
                this.userLoginStatusService.getUserProfile().subscribe(profile => {
                    this.profile = profile;
                    localStorage.setItem('profile', JSON.stringify(profile));
                })
            );
        }
    }

    getProfilePicture() {
        let profilePicture = localStorage.getItem('profilePicture');
        if (profilePicture != null || profilePicture != undefined) {
            this.profilePicture = this.domSanitizer.bypassSecurityTrustResourceUrl('data:image/jpeg;base64,' + profilePicture);
        }
        else {
            this.sub$.push(
                this.userLoginStatusService.getUserPhoto().subscribe({
                    next: (photo) => {
                        let reader = new FileReader();
                        reader.readAsDataURL(photo);
                        reader.onload = () => {
                            this.profilePicture = this.domSanitizer.bypassSecurityTrustResourceUrl(reader.result as string);
                            if (this.profilePicture == null) {
                                this.profilePicture = this.domSanitizer.bypassSecurityTrustResourceUrl('assets/default-user.jpg');
                            }
                            if (reader.result != null) {
                                localStorage.setItem('profilePicture', reader.result.toString().split(',')[1]);
                            }
                        };
                    },
                    error: (error) => {
                        this.profilePicture = this.domSanitizer.bypassSecurityTrustResourceUrl('assets/default-user.jpg');
                    }
                })
            );
        }
    }
}

Identity Provider

None

Source

External (Customer)

abhisheknpj commented 1 year ago

I updated the msal-angular to 2.5.3 and msal-browser to 2.33.0 and the issue is fixed