mauriciovigolo / keycloak-angular

Easy Keycloak setup for Angular applications.
MIT License
727 stars 280 forks source link

keycloak.init config that depends on another APP_INITIALIZER #38

Closed findjonmos closed 6 years ago

findjonmos commented 6 years ago

Hello, I have been trying to figure this out on my own for a while now.

I already have an APP_INITIALIZER that uses a service to fetch a config object from a .json file in the root of my project. I want to use the config information loaded as part of the config for keycloak but I cannot get it to work. I had an idea to run the keycloak.init inside the config service, and it works and allows me to login and see my app but then the request for the user info does not work, and when I check the console network tab I can see that the access headers are no longer present. If I just go back to having two APP_INITILIZER's everything works and including the call to get the user details.

Has anyone got more experience than me with this issue?

mauriciovigolo commented 6 years ago

Hello @findjonmos,

I've developed an angular components library at work and I does exactly what you are doing, loads an app-config.json file and uses part of the config data to initialize the keycloak.init. In our case the library is working as expected.

I think it may be the order of the calls or some kind of configuration you have in your app. Is there a way to share an example to reproduce the error?

findjonmos commented 6 years ago

Hey @mauriciovigolo , I will try to get you some source code to show what I was up to.

Is your example part of the repo here that I can look at?

mauriciovigolo commented 6 years ago

Great @findjonmos,

The example in this repo doesn't have this functionality (load an app-config.json file). I can implement it and release a new version. I will take a look on this.

Tks!!

mauriciovigolo commented 6 years ago

@findjonmos,

About this issue, I will share some code on how I handled this situation. But this is not needed in most of the cases, as for common situations the recommended way to get the config values is through the angular environment file. To better show this common scenario, I've changed the example (keycloak-heroes) to instead of getting the keycloak config hardcoded, get from environment file.

Coming back to the original question of using a config file, the reason it was necessary to create an AppConfigService was that I needed to get some values from the webapp, but the initializer function was located in a separate library that we built to handle the company's common Angular components. So as the keycloak-angular initialization is done inside this common library I could not get the webapp environment values. Because of this, we create an assets/app-config.json file in every application which is loaded inside the AppConfigService.

I'm sharing a similar code, so you can take a look on the idea:


export function initializer(
  keycloak: KeycloakService,
  appConfig: AppConfigService,
): () => Promise<any> {
  return (): Promise<any> => {
    return new Promise(async (resolve, reject) => {
      let config: AppConfig;
      let env: EnvValues;

      try {
        config = await appConfig.getConfigValue();
      } catch (error) {
        // Here you should properly deal with the error
        reject();
        return;
      }

      try {
        await keycloak.init({
          config: {
            url: config.keycloakUrl,
            realm: config.realm,
            clientId: config.clientId
          },
          initOptions: {
            onLoad: 'login-required',
            checkLoginIframe: false
          }
        });

        resolve();
      } catch (error) {
        // Here you should properly deal with the error
        reject();
      }
    });
  };
}

Let me know if this answer helps you and if you succeed.

If you need any further assistance I'm available to help.

Thanks!

findjonmos commented 6 years ago

@mauriciovigolo Thank you very much for doing this. I will take some time soon to try and get this example working for me. Thanks very much :)

findjonmos commented 6 years ago

@mauriciovigolo Just wanted to come back to tell you that the solution you gave me worked nicely. Thank you for your help. I had a curiosity however in regards to how you handled any errors...I had an idea to try to force a redirect to a html page separate of angular since the project would not have booted up. Have you done something different?

mauriciovigolo commented 6 years ago

@findjonmos, great that it helped!

About the way to handle errors, it depends in which layer you are talking about. If it is an error during the login process, Keycloak already bundles the error workflow and templates, so you could customize the template as mentioned at keycloak themes section.

If it is during the app initialization function (as mentioned above), you could also send the angular Router as a parameter, for example:

export function initializer(
  keycloak: KeycloakService,
  appConfig: AppConfigService,
  router: Router
): () => Promise<any> {
// ....
}

If any errors happen and your intention is to redirect to another route, you have the tools to do it.

jacksloan commented 6 years ago

@findjonmos Just to throw out another idea:

In our app we have a few init steps that depend on a config.json similar to your situation. It was starting to get a bit messy having everything in a single initializer so we decided to split things out.

Basically you can expose an observable in your config service. By subscribing to that observable in other dependent steps you can trigger them to load appropriately.

Complete example: https://stackblitz.com/edit/angular-multiple-app-init-steps

The gist of it:

// first fake service exposes an observable to notify others of its completion
export function secondInitializerFn(firstFakeService: FirstFakeService, secondFakeService): () => Promise<any> {
    return (): Promise<any> => {
        return new Promise(async (resolve, reject) => {

          firstFakeService.configDoneSubject$
                .pipe(
                    tap(isLoaded => console.log('first config loaded: ' + isLoaded)),
                    skipWhile(isLoaded => isLoaded === false), // only trigger secondConfig load once firstConfig is done
                    switchMap(() => (from(secondFakeService.secondConfig()))))
                .subscribe(resolve, reject);
        });
    };
}

@NgModule({
...
...
...
  providers: [
    FirstFakeService,
    SecondFakeService,
    {
            provide: APP_INITIALIZER,
            useFactory: firstInitializerFn,
            multi: true,
            deps: [FirstFakeService]
    },
// we have to make the second service wait for the first service in the secondInitializerFn
    {
            provide: APP_INITIALIZER,
            useFactory: secondInitializerFn,
            multi: true,
            deps: [FirstFakeService, SecondFakeService]
    },
  ],
})
export class AppModule { }
mauriciovigolo commented 6 years ago

@findjonmos, I'm closing this issue, since it seems to be solved. If you have any issues in the future we will be here to help. Thanks!