Skip to content

Can't Use Custom App Initializer with HttpClient #3714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
brandonsmith86 opened this issue Jun 4, 2021 · 12 comments
Closed

Can't Use Custom App Initializer with HttpClient #3714

brandonsmith86 opened this issue Jun 4, 2021 · 12 comments
Assignees
Labels
answered Question has received "first qualified response" msal-angular Related to @azure/msal-angular package msal-browser Related to msal-browser package Needs: Author Feedback Awaiting response from issue author no-issue-activity Issue author has not responded in 5 days question Customer is asking for a clarification, use case or information.

Comments

@brandonsmith86
Copy link

brandonsmith86 commented Jun 4, 2021

Core Library

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

Core Library Version

2.14.2

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

2.0.0

Description

I am currently using msal-angular v2.0, and configuring everything via APP_INITIALIZER providers (including HTTP_INTERCEPTORS).

export const APP_INITIALIZER_PROVIDERS: Provider[] = [
  {
    provide: HTTP_INTERCEPTORS,
    useClass: MsalInterceptor,
    multi: true
  },
  {
    provide: MSAL_INSTANCE,
    useFactory: MSALInstanceFactory
  },
  {
    provide: MSAL_GUARD_CONFIG,
    useFactory: MSALGuardConfigFactory
  },
  {
    provide: MSAL_INTERCEPTOR_CONFIG,
    useFactory: MSALInterceptorConfigFactory
  },
  MsalGuard,
  MsalBroadcastService,
  MsalService,
  UserServiceInitializer
];

This all works great, until I try to add another APP_INITIALIZER that uses HttpClient (notice UserServiceInitializer in the previous code block). The purpose of this initializer is to get additional user info (by making an API call). The API call needs to have the access token included in the header (provided by MsalInterceptor). I have tried two different approaches (listed below), and both have failed. I would really appreciate any feedback that you can give.

  1. Immediately attempting to make API call within Provider. This causes the app to throw an error as soon as httpClient.get() is called: "BrowserAuthError: interaction_in_progress: Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API."
  useFactory(appInitializer: UserService, msalBroadcast: MsalBroadcastService) {
    return (
      function(): Promise<User> {
        return appInitializer.InitUserInfo();
      }
    );
  }
  1. Waiting for Interactions to complete before making API call. In this case, the InitUserInfo method never gets called, because the observable never matches InteractionStatus.None. I am guessing this is because the app hasn't finished initializing. A bit of a chicken and the egg situation.
  useFactory(appInitializer: UserService, msalBroadcast: MsalBroadcastService) {
    return (
      function(): Promise<User> {
        return new Promise<User>(resolve => {
          msalBroadcast.inProgress$
            .pipe(filter((status: InteractionStatus) => status === InteractionStatus.None))
            .subscribe(() => {
              console.log("Done waiting to initialize UserService");
              appInitializer.InitUserInfo().then(user => resolve(user));
            });
        });
      }
    );
  }

MSAL Configuration

{
  auth: {
    clientId: OAuthSettings.clientId,
    authority: OAuthSettings.authority,
    redirectUri: OAuthSettings.redirectUri,
    navigateToLoginRequestUrl: true,
    postLogoutRedirectUri: OAuthSettings.postLogoutRedirectUri
  },
  cache: {
    cacheLocation: BrowserCacheLocation.LocalStorage,
    storeAuthStateInCookie: false
  },
  system: {
    loggerOptions: {
      loggerCallback: LoggerCallback,
      logLevel: LogLevel.Info,
      piiLoggingEnabled: false
    }
  }
}

Relevant Code Snippets

/**
   * Used for app initializer.  Gets additional user info from API.
   */
  public async InitUserInfo(): Promise<User> {
    const user = await this.GetUser().toPromise();
    if (!user) {
      this.AuthenticatedUser = new User();
      return user;
    }
    this.AuthenticatedUser = user;
    this.settings.UserSettings(this.AuthenticatedUser);
    return user;
  }

  /**
   * GET the logged in user's data from API. This will use the authorization token used in login.
   */
  public GetUser(userId: string = null): Observable<User> {
    const url = `${environment.PricingApi}User` + (userId ? `/${userId}` : "");
    // ERROR IS THROWN WHEN this.httpClient.get() EXECUTES
    return this.httpClient.get<{ data: any }>(url).pipe(
      map((res: { data: any }) => this.TransformUserData(res.data)),
      catchError(err => {
        this.logError(err);
        return of(new User());
      })
    );
  }

Identity Provider

Azure AD / MSA

Source

External (Customer)

@brandonsmith86 brandonsmith86 added the question Customer is asking for a clarification, use case or information. label Jun 4, 2021
@github-actions github-actions bot added msal-angular Related to @azure/msal-angular package msal-browser Related to msal-browser package labels Jun 4, 2021
@brandonsmith86 brandonsmith86 changed the title Need Help with Custom App Initializer Can't Use Custom App Initializer with HttpClient Jun 4, 2021
@nicolaszawada
Copy link

nicolaszawada commented Jun 7, 2021

I am having the same issue.
I have noticed this is because of the initialNavigation: isIframe ? 'disabled' : 'enabled' in the AppRouterModule.

When removing this line, the wait for the APP_INITIALIZER is done correctly. However removing this is no option, because otherwise other MSAL related stuff is not working correctly.

I am able to fix this by fetching the relevant config in the main.ts file, providing it via an Injection Token and injecting the token back in the relevant service where you need the config.
main.ts

fetch('/api/global/msal/config')
  .then(response => response.json())
  .then(config => {

    if (environment.production) {
      enableProdMode();
    }

    if (!isIE && !isBot) {
      platformBrowserDynamic([
        { provide: 'BASE_URL', useFactory: getBaseUrl, useValue: null, deps: [] },
        { provide: MY_MSAL_CONFIG, useValue: config }
      ]).bootstrapModule(AppModule)
        .catch(err => console.log(err));
    }
  });

our service
constructor(@Inject(MY_MSAL_CONFIG) private readonly settings: any) { }

@brandonsmith86
Copy link
Author

Interesting @nicolaszawada. I don't have initialNavigation set in my app router module and am still seeing the issue with APP_INITIALIZERS not waiting. Based on your info here, I tried setting initialNavigation to 'enabledBlocking', since that is supposed to be the equivalent of 'enabled' in the latest version. I noticed no difference; app initializer is still running before authentication is complete.

@samuelkubai
Copy link
Contributor

@brandonsmith86 to successfully initialize the application you will need to dynamically load the configuration as shown here using the fetch API to avoid the issue with circular dependency.

@ghost ghost added answered Question has received "first qualified response" Needs: Author Feedback Awaiting response from issue author labels Jun 8, 2021
@brandonsmith86
Copy link
Author

@samuelkubai I'm not attempting to load dynamic msal configuration. I am attempting to use one of my own app initializers after MSAL has initialized. However, my initializer is executing before the MSAL initializers have finished.

@ghost ghost added Needs: Attention 👋 Awaiting response from the MSAL.js team and removed Needs: Author Feedback Awaiting response from issue author labels Jun 8, 2021
@samuelkubai samuelkubai assigned tnorling and samuelkubai and unassigned tnorling Jun 9, 2021
@jasonnutter
Copy link
Contributor

@brandonsmith86 Can you share your MSAL Angular configuration (specifically for teh guard and interceptor)?

@ghost ghost added Needs: Author Feedback Awaiting response from issue author and removed Needs: Attention 👋 Awaiting response from the MSAL.js team labels Jun 16, 2021
@brandonsmith86
Copy link
Author

@brandonsmith86 Can you share your MSAL Angular configuration (specifically for teh guard and interceptor)?

export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap: ResourceSettings.protectedResourceMap
  };
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    authRequest: {
      scopes: OAuthSettings.scopes
    }
  };
}

@ghost ghost added Needs: Attention 👋 Awaiting response from the MSAL.js team and removed Needs: Author Feedback Awaiting response from issue author labels Jun 18, 2021
@jasonnutter
Copy link
Contributor

jasonnutter commented Jun 21, 2021

@brandonsmith86 Thanks. Applications using redirect-based navigation need to invoke handleRedirectObservable (or handleRedirectPromise to process any in progress interactions before making further MSAL calls (the guidance about waiting for interaction status to be none assumes your app is separately invoking handleRedirectObservable, which will emit the needed events). Please try invoking handleRedirectObservable (or handleRedirectPromise) before making http requests that need the interceptor or checking if there are cached accounts.

@ghost ghost added Needs: Author Feedback Awaiting response from issue author and removed Needs: Attention 👋 Awaiting response from the MSAL.js team labels Jun 21, 2021
@brandonsmith86
Copy link
Author

@jasonnutter Isn't that the same as #2 in my description? In the provider, I am waiting for interaction to be set to none. However, it never happens. I can only assume it is because the app has to finish initializing.

 useFactory(appInitializer: UserService, msalBroadcast: MsalBroadcastService) {
    return (
      function(): Promise<User> {
        return new Promise<User>(resolve => {
          msalBroadcast.inProgress$
            .pipe(filter((status: InteractionStatus) => status === InteractionStatus.None))
            .subscribe(() => {
              console.log("Done waiting to initialize UserService");
              appInitializer.InitUserInfo().then(user => resolve(user));
            });
        });
      }
    );
  }

@ghost ghost added Needs: Attention 👋 Awaiting response from the MSAL.js team Needs: Author Feedback Awaiting response from issue author and removed Needs: Author Feedback Awaiting response from issue author labels Jun 21, 2021
@ghost ghost removed the Needs: Attention 👋 Awaiting response from the MSAL.js team label Jun 21, 2021
@jasonnutter
Copy link
Contributor

I can only assume it is because the app has to finish initializing.

handleRedirectPromise is the function that performs the needed initializing to emit the events your code is waiting for.

@brandonsmith86
Copy link
Author

handleRedirectPromise is the function that performs the needed initializing to emit the events your code is waiting for.

So you're saying I can await handleRedirectPromise within my initializer and everything should start working?

@ghost ghost added Needs: Attention 👋 Awaiting response from the MSAL.js team and removed Needs: Author Feedback Awaiting response from issue author labels Jun 21, 2021
@jasonnutter
Copy link
Contributor

handleRedirectPromise is the function that performs the needed initializing to emit the events your code is waiting for.

So you're saying I can await handleRedirectPromise within my initializer and everything should start working?

I haven't had a chance to test this myself, but in theory, yes. Let us know if that doesn't work.

@ghost ghost added Needs: Author Feedback Awaiting response from issue author and removed Needs: Attention 👋 Awaiting response from the MSAL.js team labels Jun 21, 2021
@ghost
Copy link

ghost commented Jun 27, 2021

@brandonsmith86 This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.

@ghost ghost added the no-issue-activity Issue author has not responded in 5 days label Jun 27, 2021
@ghost ghost closed this as completed Jul 4, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Jul 11, 2021
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
answered Question has received "first qualified response" msal-angular Related to @azure/msal-angular package msal-browser Related to msal-browser package Needs: Author Feedback Awaiting response from issue author no-issue-activity Issue author has not responded in 5 days question Customer is asking for a clarification, use case or information.
Projects
None yet
Development

No branches or pull requests

5 participants