diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index 284649c..afb6f72 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -5,11 +5,14 @@ import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@a import { provideAnimations } from '@angular/platform-browser/animations'; import { KeycloakService } from 'keycloak-angular'; import { environment } from '../environments/environment'; +import { customKeycloakAdapter } from '../keycloak'; import { routes } from './app.routes'; import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { KeycloakInitializerService } from './services/keycloak-initializer.service'; import { SelectOptionsService } from './services/select-options.service'; +import { createLogger } from './utils/utils'; // provideClientHydration() +const logger = createLogger('ApplicationConfig'); export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withInterceptorsFromDi()), @@ -17,9 +20,10 @@ export const appConfig: ApplicationConfig = { { provide: APP_INITIALIZER, // useFactory: initializeKeycloak, - useFactory: (keycloakInitializer: KeycloakInitializerService) => async () => await keycloakInitializer.initialize(), + //useFactory: initializeKeycloak, + useFactory: initializeKeycloak3, multi: true, - // deps: [KeycloakService], + //deps: [KeycloakService], deps: [KeycloakInitializerService], }, { @@ -49,9 +53,32 @@ function initServices(selectOptions: SelectOptionsService) { await selectOptions.init(); }; } +export function initializeKeycloak3(keycloak: KeycloakInitializerService) { + return () => keycloak.initialize(); +} +export function initializeKeycloak2(keycloak: KeycloakService): () => Promise { + 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) { return async () => { + logger.info(`###>calling keycloakService init ...`); const authenticated = await keycloak.init({ config: { url: environment.keycloak.url, @@ -63,6 +90,6 @@ function initializeKeycloak(keycloak: KeycloakService) { silentCheckSsoRedirectUri: (window).location.origin + '/assets/silent-check-sso.html', }, }); - console.log(`--->${authenticated}`); + logger.info(`+++>${authenticated}`); }; } diff --git a/bizmatch/src/app/guards/auth.guard.ts b/bizmatch/src/app/guards/auth.guard.ts index e20509e..1d71b72 100644 --- a/bizmatch/src/app/guards/auth.guard.ts +++ b/bizmatch/src/app/guards/auth.guard.ts @@ -1,8 +1,9 @@ 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 { KeycloakInitializerService } from '../services/keycloak-initializer.service'; - +import { createLogger } from '../utils/utils'; +const logger = createLogger('AuthGuard'); @Injectable({ providedIn: 'root', }) @@ -11,14 +12,30 @@ export class AuthGuard extends KeycloakAuthGuard { super(router, keycloak); } - async isAccessAllowed(): Promise { - if (!this.keycloakInitializer.isInitialized()) { - await this.keycloakInitializer.initialize(); + async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + logger.info(`--->AuthGuard`); + 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(); - if (!authenticated) { - await this.router.navigate(['/home']); + if (!this.authenticated && !authenticated) { + 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)); } } diff --git a/bizmatch/src/app/services/keycloak-initializer.service.ts b/bizmatch/src/app/services/keycloak-initializer.service.ts index f744d4b..b4b571b 100644 --- a/bizmatch/src/app/services/keycloak-initializer.service.ts +++ b/bizmatch/src/app/services/keycloak-initializer.service.ts @@ -5,39 +5,60 @@ import { createLogger } from '../utils/utils'; const logger = createLogger('KeycloakInitializerService'); @Injectable({ providedIn: 'root' }) export class KeycloakInitializerService { - private initialized = false; + public initialized = false; constructor(private keycloakService: KeycloakService) {} - async initialize(): Promise { - if (this.initialized) { - return; - } - - const authenticated = await this.keycloakService.init({ - config: { - url: environment.keycloak.url, - realm: environment.keycloak.realm, - clientId: environment.keycloak.clientId, - }, - initOptions: { - onLoad: 'check-sso', - silentCheckSsoRedirectUri: (window).location.origin + '/assets/silent-check-sso.html', - flow: 'implicit', - }, - // initOptions: { - // pkceMethod: 'S256', - // redirectUri: environment.keycloak.redirectUri, - // checkLoginIframe: false, - // }, + async initialize(): Promise { + return new Promise(async (resolve, reject) => { + try { + await this.keycloakService.init({ + config: { + url: environment.keycloak.url, + realm: environment.keycloak.realm, + clientId: environment.keycloak.clientId, + }, + initOptions: { + onLoad: 'check-sso', + silentCheckSsoRedirectUri: (window).location.origin + '/assets/silent-check-sso.html', + // flow: 'implicit', + }, + }); + this.initialized = true; + resolve(true); + } catch (error) { + reject(error); + } }); - const token = await this.keycloakService.getToken(); - logger.info(`--->${authenticated}:${token}`); + // 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: (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; + // this.initialized = true; } - isInitialized(): boolean { - return this.initialized; - } + // isInitialized(): boolean { + // return this.initialized; + // } } diff --git a/bizmatch/src/keycloak.ts b/bizmatch/src/keycloak.ts new file mode 100644 index 0000000..9033180 --- /dev/null +++ b/bizmatch/src/keycloak.ts @@ -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 = Partial | (() => Partial); +/** + * Create and immediately resolve a KeycloakPromise + */ +const createPromise = () => new Promise(resolve => resolve()); +/** + * Resolve OptionsOrProvider: if it's an function, execute it, otherwise return it. + */ +const resolveOptions = (opt: OptionsOrProvider): Partial => (typeof opt === 'function' ? opt() : opt); +/** + * + * Update options with the overrides given as OptionsOrProvider + * + * @param options + * @param overrides + * @returns + */ +const updateOptions = (options: T, overrides: OptionsOrProvider): T => Object.assign(options ?? {}, 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 = {}, + logoutOptions: OptionsOrProvider = {}, + registerOptions: OptionsOrProvider = {}, +): KeycloakAdapter { + return { + login: (options?: KeycloakLoginOptions): Promise => { + updateOptions(options, loginOptions); + logger.info('Executing login. Options: ', options); + window.location.replace(kc().createLoginUrl(options)); + return createPromise(); + }, + logout: (options?: KeycloakLogoutOptions): Promise => { + updateOptions(options, logoutOptions); + logger.info('Executing logout. Options: ', options); + window.location.replace(kc().createLogoutUrl(options)); + return createPromise(); + }, + register: (options?: KeycloakRegisterOptions): Promise => { + updateOptions(options, registerOptions); + logger.info('Executing register. Options: ', options); + window.location.replace(kc().createRegisterUrl(options)); + return createPromise(); + }, + accountManagement: (): Promise => { + 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; + }, + }; +}