AzureAD / microsoft-authentication-library-for-js

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

BrowserAuthError: interaction_in_progress: Interaction is currently in progress #7040

Closed patolax closed 4 months ago

patolax commented 4 months ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

2.28.1

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

2.4.3

Public or Confidential Client?

Public

Description

After user login and 2 days after when token has expired when the page is reloaded, it takes a long time to redirect to b2c login page. Sometimes if we click on page then redirection takes place otherwise no change. Console is showin this error.

Error Message

BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API. For more visit: aka.ms/msaljs/browser-errors. BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API. For more visit: aka.ms/msaljs/browser-errors.

MSAL Logs

www.redacted.com-1713500518270.log

Network Trace (Preferrably Fiddler)

MSAL Configuration

import { MsalGuardConfiguration, MsalInterceptorConfiguration } from '@azure/msal-angular';
import { Configuration, PublicClientApplication, InteractionType, IPublicClientApplication, LogLevel, RedirectRequest, PopupRequest } from '@azure/msal-browser';

export const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;

export const b2cPolicies = {
  names: {
    signUpSignIn: 'b2c_1_login',
    resetPassword: 'b2c_1_reset',
  },
  authorities: {
    signUpSignIn: {
      authority: 'https://redacted.b2clogin.com/redacted.onmicrosoft.com/b2c_1_login'
    },
    resetPassword: {
      authority: 'https://redacted.b2clogin.com/redacted.onmicrosoft.com/b2c_1_reset'
    }
  },
  authorityDomain: "redacted.b2clogin.com"
};

// Config object to be passed to Msal on creation.
// For a full list of msal.js configuration parameters,
// visit https://azuread.github.io/microsoft-authentication-library-for-js/docs/msal/modules/_configuration_.html
export const msalConfig: Configuration = {
  auth: {
    clientId: 'redacted',
    authority: b2cPolicies.authorities.signUpSignIn.authority,
    knownAuthorities: [b2cPolicies.authorityDomain],
    redirectUri: 'http://localhost:7100/',
    postLogoutRedirectUri: 'http://localhost:7100/',
  },
  cache: {
    cacheLocation: 'localStorage', // This configures where your cache will be stored
    storeAuthStateInCookie: isIE,  // Set this to "true" to save cache in cookies to address trusted zones limitations in
    // IE (see: https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/Known-issues-on-IE-and-Edge-Browser)
  },
  system: {
    loggerOptions: {
      loggerCallback(logLevel: LogLevel, message: string) {
        console.log(message);
      },
      logLevel: LogLevel.Warning,
      piiLoggingEnabled: false
    }
  }
};

export const resetPasswordFlowRequest: RedirectRequest | PopupRequest = {
  authority: b2cPolicies.authorities.resetPassword.authority,
  scopes: ["openid", "profile"],
};

export const protectedResources = {
  redactedApi: {
    endpoint: "https://localhost:9000/api",
    scopes: [
      'https://redacted.onmicrosoft.com/redactedapi/s_read',

    ],
  },
};

/**
 * Here we pass the configuration parameters to create an MSAL instance.
 * For more info, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/configuration.md
 */
export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication(msalConfig);
}

/**
 * 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: [...protectedResources.redactedApi.scopes],
};

/**
 * MSAL Angular will automatically retrieve tokens for resources 
 * added to protectedResourceMap. For more info, visit: 
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/v2-docs/initialization.md#get-tokens-for-web-api-calls
 */
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  const protectedResourceMap = new Map<string, Array<string>>();

  protectedResourceMap.set(protectedResources.redactedApi.endpoint, protectedResources.redactedApi.scopes);

  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap
  };
}

/**
 * Set your default interaction type for MSALGuard here. If you have any
 * additional scopes you want the user to consent upon login, add them here as well.
 */
export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    authRequest: loginRequest
  };
}

Relevant Code Snippets

@Injectable()
export class MsalAuthService {
  title = 'Azure AD B2C';
  isIframe = false;
  loggedIn = false;
  public isAuthorized$ = new BehaviorSubject<boolean>(false);
  public acquireTokenSuccess$ = new BehaviorSubject<any>(undefined);
  private readonly logger: AbstractLogger;
  resetPasswordRequest: RedirectRequest;
  private readonly _destroying$ = new Subject<void>();

  constructor(
    @Inject(MSAL_GUARD_CONFIG) private readonly msalGuardConfig: MsalGuardConfiguration,
    private readonly msalBroadcastService: MsalBroadcastService,
    private readonly authService: MsalService,
    logSvc: LoggerService,
  ) {
    this.logger = logSvc.getLogger('MsalAuthService');
    this.isIframe = window !== window.parent && !window.opener;
    this.resetPasswordRequest = resetPasswordFlowRequest;

    this.authService.instance.enableAccountStorageEvents();

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

    this.msalBroadcastService.inProgress$
      .pipe(
        filter((status: InteractionStatus) => status === InteractionStatus.None),
        takeUntil(this._destroying$)
      )
      .subscribe(() => {
        this.logger.debug("inProgress$");
        this.checkAndSetActiveAccount();
      });

    this.msalBroadcastService.msalSubject$
      .pipe(
        filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS
          || msg.eventType === EventType.ACQUIRE_TOKEN_SUCCESS
          || msg.eventType === EventType.SSO_SILENT_SUCCESS),
        takeUntil(this._destroying$)
      )
      .subscribe((result: EventMessage) => {

        let payload = result.payload as AuthenticationResult;
        let idtoken = payload.idTokenClaims as IdTokenClaimsWithPolicyId;

        if (idtoken.acr === b2cPolicies.names.signUpSignIn || idtoken.tfp === b2cPolicies.names.signUpSignIn) {
          this.authService.instance.setActiveAccount(payload.account);
        }

        if (idtoken.acr === b2cPolicies.names.resetPassword || idtoken.tfp === b2cPolicies.names.resetPassword) {
          let signUpSignInFlowRequest: RedirectRequest | PopupRequest = {
            authority: b2cPolicies.authorities.signUpSignIn.authority,
            scopes: [...protectedResources.rrcApi.scopes],
            prompt: PromptValue.LOGIN // force user to reauthenticate with their new password
          };

          this.login(signUpSignInFlowRequest);
        }

        return result;
      });

    this.msalBroadcastService.msalSubject$
      .pipe(
        filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_FAILURE || msg.eventType === EventType.ACQUIRE_TOKEN_FAILURE),
        takeUntil(this._destroying$)
      )
      .subscribe((result: EventMessage) => {
        // Check for forgot password error
        // Learn more about AAD error codes at https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes
        if (result.error && result.error.message.indexOf('AADB2C90118') > -1) {
          let resetPasswordFlowRequest: RedirectRequest | PopupRequest = {
            authority: b2cPolicies.authorities.resetPassword.authority,
            scopes: [],
          };

          this.login(resetPasswordFlowRequest);
        };
      });
  }

  async getAccessToken(): Promise<string> {
    let authResponse: AuthenticationResult | null = null;
    let accessToken = "";
    try {
      authResponse = await this.authService.instance.acquireTokenSilent({
        account: this.authService.instance.getActiveAccount()!,
        scopes: protectedResources.rrcApi.scopes,
      });

    } catch (error) {
      if (error instanceof InteractionRequiredAuthError) {
        authResponse = await this.authService.instance.acquireTokenPopup({
          scopes: protectedResources.rrcApi.scopes,
        });
      }
      this.logger.debug(error);
    }
    accessToken = authResponse ? authResponse.accessToken : "";
    if (authResponse) {
      this.acquireTokenSuccess$.next(authResponse.accessToken);
    }
    accessToken = authResponse ? authResponse.accessToken : "";
    return accessToken;
  }

  getClaims(): any {
    if (this.loggedIn) {
      const account = this.authService.instance.getActiveAccount();
      this.logger.debug("msalBroadcastService getClaims account", account);
      if (account) {
        return account.idTokenClaims;
      }
    }
    return null;
  }

  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();
    this.logger.debug("checkAndSetActiveAccount", activeAccount);
    this.loggedIn = !!activeAccount;
    this.isAuthorized$.next(this.loggedIn);

    if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
      let account = this.authService.instance.getAllAccounts()[0];
      this.authService.instance.setActiveAccount(account);
      this.loggedIn = !!account;
      this.isAuthorized$.next(this.loggedIn);
    }
  }

  resetPassword() {
    if (this.resetPasswordRequest) {
      this.authService.instance.loginRedirect(this.resetPasswordRequest);
    }
  }

  login(userFlowRequest?: RedirectRequest | PopupRequest) {
    if (this.msalGuardConfig.interactionType === InteractionType.Popup) {
      if (this.msalGuardConfig.authRequest) {
        this.authService.loginPopup({ ...this.msalGuardConfig.authRequest, ...userFlowRequest } as PopupRequest)
          .subscribe((response: AuthenticationResult) => {
            this.logger.debug("login AuthenticationResult", response.state);
            this.authService.instance.setActiveAccount(response.account);
          });
      } else {
        this.authService.loginPopup(userFlowRequest)
          .subscribe((response: AuthenticationResult) => {
            this.logger.debug("loginPopup AuthenticationResult", response.state);
            this.authService.instance.setActiveAccount(response.account);
          });
      }
    } else {
      if (this.msalGuardConfig.authRequest) {
        this.authService.instance.loginRedirect({ ...this.msalGuardConfig.authRequest, ...userFlowRequest } as RedirectRequest);
      } else {
        this.authService.instance.loginRedirect(userFlowRequest);
      }
    }
  }

  logout() {
    this.authService.instance.logout();
  }
}

Reproduction Steps

  1. Login
  2. wait two days
  3. reload the page

Expected Behavior

Recirect the user to b2c login page without significant wait. Currently it stays in loading state.

Identity Provider

Azure B2C Basic Policy

Browsers Affected (Select all that apply)

Chrome, Firefox, Edge

Regression

no

Source

External (Customer)

tnorling commented 4 months ago

Looks like you're firing several requests at the same time which are failing and each falling back to interaction. Only one interaction is allowed at a time. Based on your code snippets above it looks like you have fallback logic that invokes acquireTokenPopup but you're not checking if an interaction is already in progress. See here for more.

Relevant logs for reference:

main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [c6f13c73-9428-43bf-ace0-f5a392af2a1c] : @azure/msal-browser@2.38.4 : Verbose - acquireTokenRedirect called main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-browser@2.38.4 : Verbose - preflightBrowserEnvironmentCheck started main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-browser@2.38.4 : Verbose - preflightInteractiveRequest called, validating app environment main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-browser@2.38.4 : Verbose - getAllAccounts called main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-common@13.3.1 : Info - CacheManager:getIdToken - Returning id token main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-browser@2.38.4 : Info - Emitting event: msal:acquireTokenStart main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-browser@2.38.4 : Verbose - Emitting event to callback 7b192272-3903-443e-97aa-cc9ac517cf6f: msal:acquireTokenStart main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-angular@2.5.13 : Verbose - BroadcastService - msal:acquireTokenStart results in setting inProgress from none to acquireToken main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [6d36b58d-00f6-4191-8e21-79a1dc35a21f] : msal.js.browser@2.38.4 : Verbose - initializeAuthorizationRequest called main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [6d36b58d-00f6-4191-8e21-79a1dc35a21f] : msal.js.browser@2.38.4 : Verbose - getRedirectUri called main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [6d36b58d-00f6-4191-8e21-79a1dc35a21f] : msal.js.browser@2.38.4 : Verbose - Initializing BaseAuthRequest main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [6d36b58d-00f6-4191-8e21-79a1dc35a21f] : msal.js.browser@2.38.4 : Verbose - Authentication Scheme wasn't explicitly set in request, defaulting to "Bearer" request main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-angular@2.5.13 : Error - Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve. main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-angular@2.5.13 : Error - Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve. main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-angular@2.5.13 : Error - Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve. main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : @azure/msal-angular@2.5.13 : Error - Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve. main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [6d36b58d-00f6-4191-8e21-79a1dc35a21f] : msal.js.browser@2.38.4 : Verbose - Setting validated request account main.12a91667d91c2598.js:1 [Fri, 19 Apr 2024 04:21:06 GMT] : [d025eedc-2bbe-4375-8d0d-7370a95b27e2] : @azure/msal-browser@2.38.4 : Verbose - acquireTokenPopup called