waiting for initialization

This commit is contained in:
Andreas Knuth 2024-05-22 13:32:17 -05:00
parent 7fdc87fb0b
commit d6768b3da9
4 changed files with 190 additions and 39 deletions

View File

@ -5,11 +5,14 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { customKeycloakAdapter } from '../keycloak';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { LoadingInterceptor } from './interceptors/loading.interceptor';
import { KeycloakInitializerService } from './services/keycloak-initializer.service'; import { KeycloakInitializerService } from './services/keycloak-initializer.service';
import { SelectOptionsService } from './services/select-options.service'; import { SelectOptionsService } from './services/select-options.service';
import { createLogger } from './utils/utils';
// provideClientHydration() // provideClientHydration()
const logger = createLogger('ApplicationConfig');
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@ -17,9 +20,10 @@ export const appConfig: ApplicationConfig = {
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
// useFactory: initializeKeycloak, // useFactory: initializeKeycloak,
useFactory: (keycloakInitializer: KeycloakInitializerService) => async () => await keycloakInitializer.initialize(), //useFactory: initializeKeycloak,
useFactory: initializeKeycloak3,
multi: true, multi: true,
// deps: [KeycloakService], //deps: [KeycloakService],
deps: [KeycloakInitializerService], deps: [KeycloakInitializerService],
}, },
{ {
@ -49,9 +53,32 @@ function initServices(selectOptions: SelectOptionsService) {
await selectOptions.init(); await selectOptions.init();
}; };
} }
export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
return () => keycloak.initialize();
}
export function initializeKeycloak2(keycloak: KeycloakService): () => Promise<void> {
return async () => {
const { url, realm, clientId } = environment.keycloak;
const adapter = customKeycloakAdapter(() => keycloak.getKeycloakInstance(), {});
if (window.location.search.length > 0) {
sessionStorage.setItem('SEARCH', window.location.search);
}
const { host, hostname, href, origin, pathname, port, protocol, search } = window.location;
await keycloak.init({
config: { url, realm, clientId },
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.hostname === 'localhost' ? `${window.location.origin}/assets/silent-check-sso.html` : `${window.location.origin}/dealerweb/assets/silent-check-sso.html`,
adapter,
redirectUri: `${origin}${pathname}`,
},
});
};
}
function initializeKeycloak(keycloak: KeycloakService) { function initializeKeycloak(keycloak: KeycloakService) {
return async () => { return async () => {
logger.info(`###>calling keycloakService init ...`);
const authenticated = await keycloak.init({ const authenticated = await keycloak.init({
config: { config: {
url: environment.keycloak.url, url: environment.keycloak.url,
@ -63,6 +90,6 @@ function initializeKeycloak(keycloak: KeycloakService) {
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html', silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
}, },
}); });
console.log(`--->${authenticated}`); logger.info(`+++>${authenticated}`);
}; };
} }

View File

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router, UrlTree } from '@angular/router'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular'; import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
import { KeycloakInitializerService } from '../services/keycloak-initializer.service'; import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
import { createLogger } from '../utils/utils';
const logger = createLogger('AuthGuard');
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -11,14 +12,30 @@ export class AuthGuard extends KeycloakAuthGuard {
super(router, keycloak); super(router, keycloak);
} }
async isAccessAllowed(): Promise<boolean | UrlTree> { async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
if (!this.keycloakInitializer.isInitialized()) { logger.info(`--->AuthGuard`);
await this.keycloakInitializer.initialize(); while (!this.keycloakInitializer.initialized) {
logger.info(`Waiting 100 msec`);
await new Promise(resolve => setTimeout(resolve, 100));
} }
// Force the user to log in if currently unauthenticated.
const authenticated = this.keycloak.isLoggedIn(); const authenticated = this.keycloak.isLoggedIn();
if (!authenticated) { if (!this.authenticated && !authenticated) {
await this.router.navigate(['/home']); await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
// return false;
} }
return authenticated;
// Get the roles required from the route.
const requiredRoles = route.data['roles'];
// Allow the user to proceed if no additional roles are required to access the route.
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
return true;
}
// Allow the user to proceed if all the required roles are present.
return requiredRoles.every(role => this.roles.includes(role));
} }
} }

View File

@ -5,16 +5,14 @@ import { createLogger } from '../utils/utils';
const logger = createLogger('KeycloakInitializerService'); const logger = createLogger('KeycloakInitializerService');
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class KeycloakInitializerService { export class KeycloakInitializerService {
private initialized = false; public initialized = false;
constructor(private keycloakService: KeycloakService) {} constructor(private keycloakService: KeycloakService) {}
async initialize(): Promise<void> { async initialize(): Promise<boolean> {
if (this.initialized) { return new Promise<boolean>(async (resolve, reject) => {
return; try {
} await this.keycloakService.init({
const authenticated = await this.keycloakService.init({
config: { config: {
url: environment.keycloak.url, url: environment.keycloak.url,
realm: environment.keycloak.realm, realm: environment.keycloak.realm,
@ -23,21 +21,44 @@ export class KeycloakInitializerService {
initOptions: { initOptions: {
onLoad: 'check-sso', onLoad: 'check-sso',
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html', silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
flow: 'implicit', // flow: 'implicit',
}, },
// initOptions: {
// pkceMethod: 'S256',
// redirectUri: environment.keycloak.redirectUri,
// checkLoginIframe: false,
// },
}); });
const token = await this.keycloakService.getToken();
logger.info(`--->${authenticated}:${token}`);
this.initialized = true; this.initialized = true;
resolve(true);
} catch (error) {
reject(error);
}
});
// if (this.initialized) {
// return;
// }
// logger.info(`###>calling keycloakService init ...`);
// const authenticated = await this.keycloakService.init({
// config: {
// url: environment.keycloak.url,
// realm: environment.keycloak.realm,
// clientId: environment.keycloak.clientId,
// },
// initOptions: {
// onLoad: 'check-sso',
// silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
// // flow: 'implicit',
// },
// // initOptions: {
// // pkceMethod: 'S256',
// // redirectUri: environment.keycloak.redirectUri,
// // checkLoginIframe: false,
// // },
// });
// logger.info(`+++>authenticated: ${authenticated}`);
// const token = await this.keycloakService.getToken();
// logger.info(`--->${token}`);
// this.initialized = true;
} }
isInitialized(): boolean { // isInitialized(): boolean {
return this.initialized; // return this.initialized;
} // }
} }

86
bizmatch/src/keycloak.ts Normal file
View File

@ -0,0 +1,86 @@
import { KeycloakAdapter, KeycloakInstance, KeycloakLoginOptions, KeycloakLogoutOptions, KeycloakRegisterOptions } from 'keycloak-js';
import { createLogger } from './app/utils/utils';
const logger = createLogger('keycloak');
export type OptionsOrProvider<T> = Partial<T> | (() => Partial<T>);
/**
* Create and immediately resolve a KeycloakPromise
*/
const createPromise = () => new Promise<void>(resolve => resolve());
/**
* Resolve OptionsOrProvider: if it's an function, execute it, otherwise return it.
*/
const resolveOptions = <T>(opt: OptionsOrProvider<T>): Partial<T> => (typeof opt === 'function' ? opt() : opt);
/**
*
* Update options with the overrides given as OptionsOrProvider
*
* @param options
* @param overrides
* @returns
*/
const updateOptions = <T>(options: T, overrides: OptionsOrProvider<T>): T => Object.assign(options ?? <T>{}, resolveOptions(overrides));
/**
* Keycloak adapter that supports options customization.
*
* All options can either be given as lazily evaluated provider functions (that will be evaluated
* right before navigating) or eagerly evaluated objects. These options will have precedence
* over options passed by keycloak-js.
*
* Cf. https://www.keycloak.org/docs/15.0/securing_apps/#custom-adapters
*
* Actual implementation copied more or less verbatim from
* https://github.com/keycloak/keycloak-js-bower/blob/10.0.2/dist/keycloak.js#L1136
*
* @param kc Function that returns a Keycloak instance
* @param loginOptions login options
* @param logoutOptions logout options
* @param registerOptions register options
* @returns KeycloakAdapter
*/
export function customKeycloakAdapter(
kc: () => KeycloakInstance,
loginOptions: OptionsOrProvider<KeycloakLoginOptions> = {},
logoutOptions: OptionsOrProvider<KeycloakLogoutOptions> = {},
registerOptions: OptionsOrProvider<KeycloakRegisterOptions> = {},
): KeycloakAdapter {
return {
login: (options?: KeycloakLoginOptions): Promise<void> => {
updateOptions(options, loginOptions);
logger.info('Executing login. Options: ', options);
window.location.replace(kc().createLoginUrl(options));
return createPromise();
},
logout: (options?: KeycloakLogoutOptions): Promise<void> => {
updateOptions(options, logoutOptions);
logger.info('Executing logout. Options: ', options);
window.location.replace(kc().createLogoutUrl(options));
return createPromise();
},
register: (options?: KeycloakRegisterOptions): Promise<void> => {
updateOptions(options, registerOptions);
logger.info('Executing register. Options: ', options);
window.location.replace(kc().createRegisterUrl(options));
return createPromise();
},
accountManagement: (): Promise<void> => {
const accountUrl = kc().createAccountUrl();
logger.info('Executing account management');
if (typeof accountUrl !== 'undefined') {
window.location.href = accountUrl;
} else {
throw new Error('Not supported by the OIDC server');
}
return createPromise();
},
redirectUri: (options: { redirectUri: string }) => {
if (options?.redirectUri) {
return options.redirectUri;
}
if (kc().redirectUri) {
return kc().redirectUri;
}
return window.location.href;
},
};
}