-
Notifications
You must be signed in to change notification settings - Fork 289
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
Comments
I am in the same situation. I need to fetch the config from API endpoint. :-( |
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! |
Hi @mauriciovigolo , |
I’ll proceed with Option 1 and work on implementing it. A patch will be prepared for release next week. |
so for now I'm keeping version 16.1.0. |
When will the Version v19.10.0 released? |
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. |
Same here, need to load my config dynamically from an API and can't get it to work in a non-deprecated way... |
Hey! We have a similar configuration use-case. We use the external app configuration detailed here: angular/angular-cli#3855 (comment) Basically:
Our problems with the new way of settings things up:
Your proposed solution fixes this issue (if we can call
This is a bit hacky with the new config, but it is achievable.
Could you expose the
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()]),
]
}; |
Hi @mauriciovigolo, |
Hi @b-thiswatch, |
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 :-) |
Hi @mauriciovigolo , Consider the ability to disable Keycloak via config. I have multiple installations: some of them with SSO enabled; others don't. Thanks. |
Hey @goldmont, |
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. |
Hi @mauriciovigolo, 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. |
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]
}
]
};
} |
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. |
Hey everyone! Just wanted to share that after digging into keycloak-js loadConfig(), I realized we can supply a URL to the 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
|
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 :) |
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 |
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. |
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, |
@mauriciovigolo could you please extend the type of the property so we can this approach in a typesafe way as well? |
@RobSlgm thank you, appConfig as function was solution for me. |
@RobSlgm's solution works, we are using it too. |
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 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:
|
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:
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:
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.
Similarly, if you already had a bootstrap configuration object, you could write:
|
@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. |
On my project we needed to be able to configure keycloak from a config.json.
the usage in the providers:
|
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 |
Any plans when this fix will be released? |
Bug Report or Feature Request (mark with an
x
)Versions.
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.
The text was updated successfully, but these errors were encountered: