Skip to content
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

provideKeycloak with config coming from API and 404 on documentation #604

Open
1 of 2 tasks
MatteoBarbieriSacmi opened this issue Dec 30, 2024 · 33 comments
Open
1 of 2 tasks
Assignees
Labels
enhancement This issue/PR is an enhancement or new feature.
Milestone

Comments

@MatteoBarbieriSacmi
Copy link

Bug Report or Feature Request (mark with an x)

  • bug report -> please search for issues before submitting
  • feature request

Versions.

"@angular/core": "^19.0.0",
"keycloak-angular": "^19.0.2",
"keycloak-js": "^26.0.7",

Repro steps.

I cannot find an example of using new provideKeycloak function with server config coming from an api.
Before we used:
provideAppInitializer(() => {
return initKeycloak(inject(ConfigService), inject(KeycloakService)));
})
where ConfigService is the service that exposes an Observable that retrieve KeycloakConfig (url, realm, client)

furthermore by clicking Keycloak Provider
from this page
https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md#bootstrapping-keycloak-with-providekeycloak
return 404

I'm sorry but it's probably neither a bug report nor a feature request.

@plmarcelo
Copy link

I am in the same situation. I need to fetch the config from API endpoint. :-(

@mauriciovigolo mauriciovigolo self-assigned this Jan 2, 2025
@mauriciovigolo mauriciovigolo added the enhancement This issue/PR is an enhancement or new feature. label Jan 2, 2025
@mauriciovigolo
Copy link
Owner

Hey @MatteoBarbieriSacmi and @plmarcelo,

I considered this scenario while rewriting the library to support multi-tenancy. By the way, are you working in a multi-tenancy environment?

To address this use case, I’ve been exploring two potential approaches:

Function-Based Dynamic Configuration:

function provideKeycloak(options: ProvideKeycloakOptions | () => Observable<ProvideKeycloakOptions>): EnvironmentProviders;

This approach allows all configuration to be resolved dynamically, offering more flexibility.

Dynamic Config Property in Options:

export type ProvideKeycloakOptions = {
  config: KeycloakConfig | () => Observable<KeycloakConfig>;
  initOptions?: KeycloakInitOptions;
  providers?: Array<Provider | EnvironmentProviders>;
  features?: Array<KeycloakFeature>;
};

Here, the config property itself becomes dynamic, enabling runtime flexibility while keeping the rest of the options static.

Personally, I’m leaning toward the first approach as it keeps the entire configuration dynamic, which seems better suited for scenarios like multi-tenancy.

I’d love to hear feedback from the community on these options or any other suggestions you might have!

@mauriciovigolo mauriciovigolo added the need-information More information is needed label Jan 2, 2025
@plmarcelo
Copy link

Hi @mauriciovigolo ,
I like option 1. It seems more flexible. In my case, I don't use multi-tenancy, but maybe in the future, I will.

@mauriciovigolo
Copy link
Owner

I’ll proceed with Option 1 and work on implementing it. A patch will be prepared for release next week.

@mauriciovigolo mauriciovigolo removed the need-information More information is needed label Jan 2, 2025
@mauriciovigolo mauriciovigolo added this to the v19.10.0 milestone Jan 2, 2025
@MatteoBarbieriSacmi
Copy link
Author

so for now I'm keeping version 16.1.0.
As soon as I hear from you I'll try the upgrade.
thanks again

@mohamedibrahim54
Copy link

mohamedibrahim54 commented Jan 2, 2025

When will the Version v19.10.0 released?
I try some workaround but have issue in AuthGuard as no provider for keycloak. I think maybe it should wait until keycloak is initialized. so I use the legacy way.

@stevenhatfield
Copy link

stevenhatfield commented Jan 6, 2025

I am waiting for this feature as well. We won't know what the Realm or the address of the Keycloak server are until after the webapp makes a call to the API server to get the initialization data.

@b-thiswatch
Copy link

b-thiswatch commented Jan 7, 2025

Same here, need to load my config dynamically from an API and can't get it to work in a non-deprecated way...
@mauriciovigolo can you draft an example how using Option 1 would look like or will you add some docs? thanks

@Res42
Copy link

Res42 commented Jan 9, 2025

Hey!

We have a similar configuration use-case.

We use the external app configuration detailed here: angular/angular-cli#3855 (comment)

Basically:

  1. Load our config from (a docker mounted) file to a service.
  2. Use that config to initialize Keycloak.

Our problems with the new way of settings things up:

  1. We cannot use this service to configure the Keycloak settings, because we cannot inject it inside provideKeycloak. This is the same issue that was detailed here already.

Your proposed solution fixes this issue (if we can call inject in the config function).

  1. We have multiple provideAppInitializers that are required to be run serially (the keycloak config is a dependent of the general config). provideKeycloak hides the provideAppInitializer so we can't instruct it to run after the config loading step.

This is a bit hacky with the new config, but it is achievable.

If you need to control when keycloak.init() is called, omit the initOptions parameter. This gives you full control over the initialization process.

Could you expose the keycloak.init() code without the provideAppInitializer part?

const injector = inject(EnvironmentInjector);
runInInjectionContext(injector, () => features.forEach((feature) => feature.configure()));
await keycloak.init(initOptions).catch((error) => console.error('Keycloak initialization failed', error));

We do not want to copy the whole init code.

In the end the startup code would look like something like this:

export const appConfig: ApplicationConfig = {
  providers: [
    provideKeycloak(() => {
      const environmentService = inject(EnvironmentService);

      return environmentService.environment$.pipe(
        map((environment) => ({
          config: {
            url: environment.authServerUrl,
            realm: environment.realm,
            clientId: environment.clientId,
          },
        })),
      );
    }),
    provideAppInitializersSerially([
      () => initializeEnvironment(),
      () => {
        const window = inject(WA_WINDOW);

        // something like `provideKeycloakInAppInitializer` without the `provideAppInitializer` part
        return initKeycloak({
          onLoad: 'check-sso',
          silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
        });
      },
    ]),
  ]
};

This is our current startup code for context:

import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class EnvironmentService {
  environment = environment;

  async loadEnvironment() {
    const response = await fetch('./environments/environment.json');

    if (response.ok) {
      const environmentJson = (await response.json()) as Environment;
      this.environment = Object.assign(this.environment, environmentJson);
    } else {
      console.warn('Cannot find environment.json file.');
    }
  }
}

export function initializeEnvironment() {
  const environmentService = inject(EnvironmentService);
  return environmentService.loadEnvironment();
}

export function initializeKeycloak() {
  const environmentService = inject(EnvironmentService);
  const window = inject(WA_WINDOW);
  const keycloak = inject(KeycloakService);

  return keycloak.init({
    config: {
      url: environmentService.environment.authServerUrl,
      realm: environmentService.environment.realm,
      clientId: environmentService.environment.clientId,
    },
    initOptions: {
      onLoad: 'check-sso',
      silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
    },
  });
}

export type AppInitializerFn = Parameters<typeof provideAppInitializer>[0];

export function provideAppInitializersSerially(appInitializers: AppInitializerFn[]) {
  return provideAppInitializer(() => {
    const injector = inject(EnvironmentInjector);

    const evaluate = (appInitializer: AppInitializerFn): MonoTypeOperatorFunction<unknown> => {
      return (source) =>
        source.pipe(
          switchMap(() => {
            const result = runInInjectionContext(injector, () => appInitializer());
            return wrapIntoObservable(result).pipe(first());
          }),
        );
    };

    // NOTE: using of<unknown>(undefined).pipe(...appInitializers.map(evaluate)) does not work: https://github.com/ReactiveX/rxjs/issues/3989
    return appInitializers.map(evaluate).reduce((observable, operator) => observable.pipe(operator), of<unknown>(undefined));
  });
}

export const appConfig: ApplicationConfig = {
  providers: [
    importProvidersFrom(KeycloakAngularModule),
    provideAppInitializersSerially([() => initializeEnvironment(), () => initializeKeycloak()]),
  ]
};

@b-thiswatch
Copy link

Hi @mauriciovigolo,
do you have a rough timeline on when this feature will become available? Thanks!

@mauriciovigolo
Copy link
Owner

Hi @b-thiswatch,
I updated the release date for this milestone to 23.01, but I will try to release it earlier. Thank you!

@b-thiswatch
Copy link

@mauriciovigolo

Ah, right. Should have checked the milestone... sorry

Thanks. 23.01. would be fine for me, just needed a rough "Angular 19 this week or next week or next month" sort of timeline for my project :-)

@goldmont
Copy link

goldmont commented Jan 24, 2025

Hi @mauriciovigolo ,

Consider the ability to disable Keycloak via config. I have multiple installations: some of them with SSO enabled; others don't. Thanks.

@mauriciovigolo
Copy link
Owner

Hey @goldmont,
Could you provide more details on your proposal? If you're referring to adding a new config type and a flag to disable Keycloak, I’d prefer to avoid it to keep things simple and adhere to Keycloak’s standard configurations. However, I’d love to understand your use case better and see a code example—maybe there’s an alternative approach that could work for your setup.

@goldmont
Copy link

goldmont commented Jan 24, 2025

Hey @goldmont,
Could you provide more details on your proposal? If you're referring to adding a new config type and a flag to disable Keycloak, I’d prefer to avoid it to keep things simple and adhere to Keycloak’s standard configurations. However, I’d love to understand your use case better and see a code example—maybe there’s an alternative approach that could work for your setup.

Hi again,

Thank you for interesting into it.

I would love being able to enable/disable SSO via configuration. I have the same build deployed on many servers and I would like customizing, for example, the auth flow. Some deployments use SSO; others use standard email and password authentication. Thus, I ain't gonna deploy different builds only to set/unset Keycloak provider.

I just need a way to disable Keycloak via configuration leaving the provider in place.

Thanks.

@kudarias
Copy link

kudarias commented Jan 27, 2025

Hi @mauriciovigolo,
i hope that your update will take care about my case too. In our project we are using DevOps practice "build once, deploy anywhere". So configuration have to be load before application boostrap. Code below.

main.ts

fetch('config.json')
  .then((response) => response.json())
  .then((config) =>
    bootstrapApplication(AppComponent, {
      ...appConfig,
      providers: [{ provide: ENV_CONFIG, useValue: config }, ...appConfig.providers],
    }).catch((err) => console.error(err)),
  );

with old providing in app.config.ts

providers: {
...
   provideAppInitializer(provideKeycloak),
...
}

and i created my own provider

export function provideKeycloak() {
  const keycloak: KeycloakService = inject(KeycloakService);
  const config: EnvironmentConfig = inject(ENV_CONFIG);

  return keycloak.init({
    config: {
      ...config.keycloak
    },
    initOptions: {
      ...
    },
    enableBearerInterceptor: false,
  });
}

so i need inject ENV_CONFIG and im not able to do that after release new version, please take a look and just consider your new update about add observables, if it will cover this case too. Many thanks.

@RobSlgm
Copy link

RobSlgm commented Jan 30, 2025

Hi @kudarias

Just for your information.

While having the same DevOps requirement our approach is similar, but works with the current release.

main.ts (AppConfig is our interface for the config file)

fetch('/config.json')
  .then((response) => response.json())
  .then((config: AppConfig) => {
    bootstrapApplication(AppComponent, appConfig(config))
      .catch((err) => console.error(err));
  })

app.config.ts (just relevant parts, other providers omitted)

export function appConfig(config: AppConfig): ApplicationConfig {
  return {
    providers: [
      provideKeycloak(initializeKeycloak(config)),
      provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),
    ]
  };
};

initialize-keycloak.ts (simplified, merges static configuration with the dynamic configuration, parts omitted)

export function initializeKeycloak(config: AppConfig): ProvideKeycloakOptions {
  let apiRegex = new RegExp(String.raw`^${config.apiBaseUrl}(\/.*)?$`, 'i');
  const tokenCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
    urlPattern: apiRegex,
  });

  return {
    config: {
      url: config.oidcUri,
      realm: config.oidcRealm,
      clientId: config.oidcClientId
    },
    initOptions: {
       ...
    },
    features: [
     ...
    ],
    providers: [
      AutoRefreshTokenService,
      UserActivityService,
      {
         provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
         useValue: [tokenCondition]
       }
    ]
  };
}

@mauriciovigolo
Copy link
Owner

Thank you for sharing your solution, @RobSlgm!

A quick update on this issue: it’s been a busy week, so I haven't had as much time as I’d hoped to work on a fix. Last week, I implemented the initial proposed idea, but it caused some side effects and isn’t fully functional yet. Making the initialization of the providers asynchronous seems to introduce conflicts with Keycloak and other providers in the components.

I think @RobSlgm’s solution is a solid one—it doesn't require changes to types or implementation. Does anyone have any constraints or concerns about adopting it?

I'll be on vacation from today until February 8th, and I'll revisit this issue once I’m back.

@lekhmanrus
Copy link

Hey everyone! Just wanted to share that after digging into keycloak-js loadConfig(), I realized we can supply a URL to the config property instead of passing a config object directly. This is handy if you want to keep your Keycloak configuration in a separate file (e.g., /assets/configs/keycloak.json). For instance, with keycloak-angular, your setup can look like this:

export const appConfig: ApplicationConfig = {
  providers: [
    provideKeycloak({
      config: '/assets/configs/keycloak.json' as any,  // Path to your config file
      initOptions: {
        // ... (your other init options)
      }
    })
  ]
};

This way, keycloak-js automatically fetches and applies the JSON from the provided URL.

P.S.: You should use as any because currently keycloak-angular expects KeycloakConfig object for ProvideKeycloakOptions config. See ProvideKeycloakOptions:

export type ProvideKeycloakOptions = {
  config: KeycloakConfig; // Keycloak server configuration
  initOptions?: KeycloakInitOptions; // Optional initialization options
  providers?: Array<Provider | EnvironmentProviders>; // Additional Angular providers
  features?: Array<KeycloakFeature>; // Array of Keycloak features
};

@b-thiswatch
Copy link

Thank you for sharing your solution, @RobSlgm!

A quick update on this issue: it’s been a busy week, so I haven't had as much time as I’d hoped to work on a fix. Last week, I implemented the initial proposed idea, but it caused some side effects and isn’t fully functional yet. Making the initialization of the providers asynchronous seems to introduce conflicts with Keycloak and other providers in the components.

I think @RobSlgm’s solution is a solid one—it doesn't require changes to types or implementation. Does anyone have any constraints or concerns about adopting it?

I'll be on vacation from today until February 8th, and I'll revisit this issue once I’m back.

Both, @RobSlgm and @lekhmanrus solutions work for me. Maybe a convenience function taking the url as the parameter with a stable interface would be nice, but have a nice holiday before that :)

@YeaTiii
Copy link

YeaTiii commented Feb 3, 2025

hello,

the @RobSlgm 's solution is not good for our project as the url changes from one to another environment and we have an openApi generated Angular service to access it.

If you could bring back this, it would save us a lot of troubles.

thank you

@RobSlgm
Copy link

RobSlgm commented Feb 3, 2025

Hi @YeaTiii ,

I have the same requirement; the API URL changes from one to another environment.

In app.config.ts just add other providers. For an URL to your API for example

{ provide: API_BASE_URL, useValue: config.apiBaseUrl },

For the HTTP interceptor see above in ìnitialize-keycloak-ts` to adapt to a changing API URL.

@stevenhatfield
Copy link

stevenhatfield commented Feb 3, 2025

RobSlgm's approach worked perfectly for our needs. The Angular app is served from a Spring Boot service. I created an unsecured API endpoint "/init" that returns information for the front end, including the keycloak URL, Realm, and ClientId. The "/init" endpoint is called using the fetch() method in Rob's example, which happens when the app starts up, so it gets the config info that it needs for authentication.

Thanks Rob! You really helped us with this solution!

Best,
-Steven

@FloppyNotFound
Copy link

I realized we can supply a URL to the config property instead of passing a config object directly

@mauriciovigolo could you please extend the type of the property so we can this approach in a typesafe way as well?

@kudarias
Copy link

kudarias commented Feb 4, 2025

@RobSlgm thank you, appConfig as function was solution for me.

@RDUser-dev
Copy link

@RobSlgm's solution works, we are using it too.
However with that solution, the Keycloak configuration is downloaded from the server before the application bootstraps.
In my opinion if the provideKeycloak method instead of taking the configuration as input, took it from a KEYCLOAK_CONFIG token, it would be possible to provide the token with maximum flexibility making it possible to bootstrap the application even before having the configuration available.

@Res42
Copy link

Res42 commented Feb 4, 2025

I think @RobSlgm’s solution is a solid one—it doesn't require changes to types or implementation. Does anyone have any constraints or concerns about adopting it?

I checked out this solution and it works for simple use cases, but we have code where it does not work and would need extra hacks/workarounds.

The problem is that you can't use inject() in the appConfig() function. Or at least i don't know how to do it if there is a workaround. I can't use runInInjectionContext, because I don't have an injector.

The use case is "do not use keycloak when server side rendering, but use it on the client side"

The original code:

export function initializeKeycloak() {
  const keycloak = inject(KeycloakService);
  const platformId = inject(PLATFORM_ID);

  if (isPlatformServer(platformId)) {
    return Promise.resolve();
  }

  return keycloak.init({ /* ... */ });
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(() => initializeKeycloak()),
  ],
};

The rewrite would look like something like this:

export function appConfig(environment): ApplicationConfig {
  const platformId = inject(PLATFORM_ID); // <-- this line throws

  return {
    providers: [
      provideKeycloak({
        config: {
          url: environment.authServerUrl,
          realm: environment.realm,
          clientId: environment.clientId,
        },
        initOptions: isPlatformServer(platformId) ? undefined : {
          onLoad: 'check-sso',
          silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
        },
      }),
    ]
  };
}

But it throws this runtime error:

RuntimeError: NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext. Find more at https://angular.dev/errors/NG0203

@stevenhatfield
Copy link

stevenhatfield commented Feb 4, 2025

@RobSlgm's solution works, we are using it too. However with that solution, the Keycloak configuration is downloaded from the server before the application bootstraps. In my opinion if the provideKeycloak method instead of taking the configuration as input, took it from a KEYCLOAK_CONFIG token, it would be possible to provide the token with maximum flexibility making it possible to bootstrap the application even before having the configuration available.

I don't know if this will help you, but this is what I did in my app:

I updated the providers configuration in appConfig in app.config.ts:

export function appConfig(config: InitParams): ApplicationConfig {
    return {
        providers: [
            provideZoneChangeDetection({eventCoalescing: true}),
            provideRouter(routes),
            provideAppInitializer(() => {
                const initService = inject(InitService);
                return initService.loadInitializationData(config);
            }),
            provideKeycloak(initializeKeycloak(config)),
            provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),
        ]
    };
};

I created an initService.loadInitiationData() method and fed in the config that comes in from the fetch in main.ts. This gets the config to the app where I can place it in a global configuration object:

export class InitService {

    constructor(
        private globals: Globals) {
    }

    public loadInitializationData(config: InitParams) {
        // Now that we are in the Angular side of things, we can assign the config retrieved from the server to our globals object
        Object.assign(this.globals, config);
    }
}

The globals object is then used by the components to display info from the server (copyright, branding, app title, etc) and to know where the KC server is...

@RDUser-dev
Copy link

@RobSlgm's solution works, we are using it too. However with that solution, the Keycloak configuration is downloaded from the server before the application bootstraps. In my opinion if the provideKeycloak method instead of taking the configuration as input, took it from a KEYCLOAK_CONFIG token, it would be possible to provide the token with maximum flexibility making it possible to bootstrap the application even before having the configuration available.

I don't know if this will help you, but this is what I did in my app:

I updated the providers configuration in appConfig in app.config.ts:

export function appConfig(config: InitParams): ApplicationConfig {
    return {
        providers: [
            provideZoneChangeDetection({eventCoalescing: true}),
            provideRouter(routes),
            provideAppInitializer(() => {
                const initService = inject(InitService);
                return initService.loadInitializationData(config);
            }),
            provideKeycloak(initializeKeycloak(config)),
            provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),
        ]
    };
};

I created an initService.loadInitiationData() method and fed in the config that comes in from the fetch in main.ts. This gets the config to the app where I can place it in a global configuration object:

export class InitService {

    constructor(
        private globals: Globals) {
    }

    public loadInitializationData(config: InitParams) {
        // Now that we are in the Angular side of things, we can assign the config retrieved from the server to our globals object
        Object.assign(this.globals, config);
    }
}

The globals object is then used by the components to display info from the server (copyright, branding, app title, etc) and to know where the KC server is...

Thanks @stevenhatfield for trying, I probably explained myself badly.
I'll try to clarify with an example that takes up the use case of a previous post by @Res42.
In his example he tried to inject PLATFORM_ID in the appconfig function and obviously it can't work because the application hasn't bootstrapped yet and therefore there is no injection context created.
If provideKeycloak had taken the configuration from a hypothetical KEYCLOAK_CONFIG token, @Res42 could have written something like:

const appConfig = {
  providers: [
    {
      provide: KEYCLOAK_CONFIG,
      useFactory: (platformId: PLATFORM_ID) => {
        return {
          config: {
            url: environment.authServerUrl,
            realm: environment.realm,
            clientId: environment.clientId,
          },
          initOptions: isPlatformServer(platformId) ? undefined : {
            onLoad: 'check-sso',
            silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
          }
        };
      },
      deps: [PLATFORM_ID]
    },
    provideKeycloak(),
  ]
}

bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));

Similarly, if you already had a bootstrap configuration object, you could write:

const appConfig = {
  providers: [
    {
      provide: KEYCLOAK_CONFIG,
      useValue:   {
          config: {
            url: environment.authServerUrl,
            realm: environment.realm,
            clientId: environment.clientId,
          },
          initOptions: {
            onLoad: 'check-sso',
            silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
          }
      }
    },
    provideKeycloak(),
  ]
}

bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));

@nicolaric-akenza
Copy link

@RobSlgm s solution is a nice one but unfortunately is does not work in our complex multi-app mono repository. we use keycloak-angular in a shared library, and being dependent on having this set up in all the applications is not ideal. we still need to be able to set the configuration dynamically based on an injection token. it would be great if there would be a solution to this soon, as it blocks our angular 19 upgrade.
i'm also willing to help out where-ever needed. lmk if you'd be happy for some helping hands.

@UrzaUrza
Copy link

UrzaUrza commented Feb 7, 2025

On my project we needed to be able to configure keycloak from a config.json.
I found that the solution was to not use provideKeycloak, instead i rewrote provideKeycloak to use useFactory instead of useValue so we can inject our tokens.

function provideKeycloakCustom(providers?: (EnvironmentProviders | Provider)[]): EnvironmentProviders {
  const keycloakFactory = (options: ProvideKeycloakOptions) => {
    return new Keycloak(options.config);
  };
  const keycloakSignalFactory = (keycloak: Keycloak) => {
    return createKeycloakSignal(keycloak);
  };

  providers = providers ?? [];

  return makeEnvironmentProviders([
    {
      provide: KEYCLOAK_EVENT_SIGNAL,
      useFactory: keycloakSignalFactory,
      deps: [Keycloak],
    },
    {
      provide: Keycloak,
      useFactory: keycloakFactory,
      deps: [keycloakConfig],
    },
    ...providers,
  ]);
}

const initKeycloak = async () => {
  const isKeycloakAvailable = inject(isKeycloakAvailableConfig);
  if (isKeycloakAvailable) {
    const keycloak = inject(Keycloak);
    const options = inject(keycloakConfig);
    const { initOptions, features = [] } = options;
    const injector = inject(EnvironmentInjector);
    runInInjectionContext(injector, () => features.forEach(feature => feature.configure()));
    await keycloak.init(initOptions).catch(error => console.error('Keycloak initialization failed', error));
  }
};

const localhostConditionFactory = (keycloakUrlBearer: string, isKeycloakAvailableConfig: boolean) => {
  if (isKeycloakAvailableConfig) {
    return [
      createInterceptorCondition<IncludeBearerTokenCondition>({
        urlPattern: new RegExp(`^(${keycloakUrlBearer})(/.*)?$`, 'i'),
      }),
    ];
  }
  return [];
};

the usage in the providers:

const providers = [
  provideKeycloakCustom(),
  provideAppInitializer(initKeycloak),
  {
    provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
    useFactory: localhostConditionFactory,
    deps: [keycloakUrlBearer, isKeycloakAvailableConfig],
  },
....
]

@goldmont
Copy link

goldmont commented Feb 13, 2025

Hi,

here is my solution.

import { EnvironmentProviders, inject, Injectable, isDevMode, provideAppInitializer } from '@angular/core';
import { HttpBackend, HttpClient } from '@angular/common/http';
import { firstValueFrom, tap } from 'rxjs';
import Keycloak from 'keycloak-js';
import { TokenService } from './token.service';

type KeycloakConfig = {
	enabled: boolean;
	url: string;
	realm: string;
	clientId: string;
};

@Injectable({
	providedIn: 'root'
})
export class KeycloakService {

	private httpClient: HttpClient;
	private config?: KeycloakConfig;

	#keycloak?: Keycloak;

	constructor(handler: HttpBackend) {
		this.httpClient = new HttpClient(handler);
	}

	get configuration(): KeycloakConfig {
		if (!this.config) {
			throw new Error('Keycloak configuration unavailable.');
		}
		return this.config;
	}

	get keycloak(): Keycloak {
		if (!this.#keycloak) {
			throw new Error('Keycloak unavailable.');
		}
		return this.#keycloak;
	}

	set keycloak(keycloak: Keycloak) {
		this.#keycloak = keycloak;
	}

	get isAuthenticated(): boolean {
		try {
			return !!this.keycloak.authenticated;
		} catch (e) {
			return false;
		}
	}

	load() {
		return this.httpClient.get<KeycloakConfig>(`/assets/${isDevMode() ? 'keycloak.development.json' : 'keycloak.json'}`)
			.pipe(tap((res) => this.config = res));
	}
}

export const provideKeycloak = (): EnvironmentProviders => {
	return provideAppInitializer(async () => {

		const keycloakService = inject(KeycloakService);
		const tokenService = inject(TokenService);

		const config = await firstValueFrom(keycloakService.load());

		if (!config.enabled) return;

		const waitForRefreshToken = () => {
			const issuedAt = keycloakService.keycloak.tokenParsed?.iat;
			const expiresAt = keycloakService.keycloak.tokenParsed?.exp;
			if (!issuedAt || !expiresAt) return;
			setTimeout(() => {
				keycloakService.keycloak.updateToken(-1)
					.then((refreshed) => {
						if (!refreshed) {
							console.error('Failed to refresh token.');
							return;
						}
						tokenService.set(keycloakService.keycloak.token ?? '');
					})
					.catch(() => console.error('An error occurred while refreshing token.'));
			}, Math.floor((expiresAt - issuedAt) * .75) * 1000);
		};

		keycloakService.keycloak = new Keycloak({
			url: config.url,
			realm: config.realm,
			clientId: config.clientId
		});

		keycloakService.keycloak.onAuthRefreshSuccess = waitForRefreshToken;

		return keycloakService.keycloak
			.init({
				onLoad: 'login-required',
				redirectUri: `${window.location.origin}${window.location.pathname}${window.location.search}`
			})
			.then((authenticated) => {
				if (!authenticated) {
					location.reload();
					return;
				}
				tokenService.set(keycloakService.keycloak.token ?? '');
			})
			.then(waitForRefreshToken);
	});
};

Simply add provideKeycloak() to your appConfig.providers

@berkon
Copy link

berkon commented Feb 19, 2025

Any plans when this fix will be released?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement This issue/PR is an enhancement or new feature.
Projects
None yet
Development

No branches or pull requests