Compare commits

..

4 Commits

Author SHA1 Message Date
Andreas Knuth d6768b3da9 waiting for initialization 2024-05-22 13:32:17 -05:00
Andreas Knuth 7fdc87fb0b flow: 'implicit' 2024-05-22 11:40:49 -05:00
Andreas Knuth 0b7e33612a new authorization approach 2024-05-22 11:05:40 -05:00
Andreas Knuth 8fba3aa832 authGuard acc. lejdiprifti.com 2024-05-22 09:31:31 -05:00
27 changed files with 314 additions and 893 deletions

View File

@ -37,6 +37,7 @@
"dayjs": "^1.11.11",
"express": "^4.18.2",
"jwt-decode": "^4.0.0",
"keycloak-angular": "^15.2.1",
"keycloak-js": "^24.0.4",
"memoize-one": "^6.0.0",
"on-change": "^5.0.1",

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { ConfirmationService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
@ -10,7 +11,6 @@ import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model'
import build from '../build';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { KeycloakService } from './services/keycloak.service';
import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service';
import { createDefaultListingCriteria } from './utils/utils';

View File

@ -3,13 +3,16 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
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 { KeycloakService } from './services/keycloak.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,7 +20,8 @@ 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: [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<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) {
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: (<any>window).location.origin + '/assets/silent-check-sso.html',
},
});
console.log(`--->${authenticated}`);
logger.info(`+++>${authenticated}`);
};
}

View File

@ -1,7 +1,8 @@
import { Routes } from '@angular/router';
import { LogoutComponent } from './components/logout/logout.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { authGuard } from './guards/auth.guard';
import { AuthGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard';
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
@ -64,62 +65,62 @@ export const routes: Routes = [
{
path: 'account',
component: AccountComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
{
path: 'account/:id',
component: AccountComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// Create, Update Listings
{
path: 'editBusinessListing/:id',
component: EditBusinessListingComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
{
path: 'createBusinessListing',
component: EditBusinessListingComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
{
path: 'editCommercialPropertyListing/:id',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
{
path: 'createCommercialPropertyListing',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// My Listings
{
path: 'myListings',
component: MyListingComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// My Favorites
{
path: 'myFavorites',
component: FavoritesComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// EMAil Us
{
path: 'emailUs',
component: EmailUsComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// Logout
{
path: 'logout',
component: LogoutComponent,
canActivate: [authGuard],
canActivate: [AuthGuard],
},
// #########
// Pricing

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { SidebarModule } from 'primeng/sidebar';
import { KeycloakService } from '../../services/keycloak.service';
import { SharedModule } from '../../shared/shared/shared.module';
@Component({
selector: 'footer',

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { KeycloakService } from 'keycloak-angular';
import { MenuItem } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { MenubarModule } from 'primeng/menubar';
@ -10,7 +11,6 @@ import { TabMenuModule } from 'primeng/tabmenu';
import { Observable } from 'rxjs';
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { KeycloakService } from '../../services/keycloak.service';
import { map2User } from '../../utils/utils';
@Component({
selector: 'header',

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { KeycloakService } from '../../services/keycloak.service';
import { KeycloakService } from 'keycloak-angular';
@Component({
selector: 'logout',

View File

@ -1,42 +1,41 @@
import { inject } from '@angular/core';
import { CanMatchFn, Router, UrlTree } from '@angular/router';
// Services
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
import { KeycloakService } from '../services/keycloak.service';
import { createLogger } from '../utils/utils';
const logger = createLogger('authGuard');
export const authGuard: CanMatchFn = async (route, segments): Promise<boolean | UrlTree> => {
const router = inject(Router);
const keycloakService = inject(KeycloakService);
const keycloakInitializer = inject(KeycloakInitializerService);
if (!keycloakInitializer.isInitialized()) {
await keycloakInitializer.initialize();
const logger = createLogger('AuthGuard');
@Injectable({
providedIn: 'root',
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(protected override readonly router: Router, protected readonly keycloak: KeycloakService, private keycloakInitializer: KeycloakInitializerService) {
super(router, keycloak);
}
logger.info('###-> calling isLoggedIn()');
const authenticated = keycloakService.isLoggedIn();
if (!authenticated) {
console.log(window.location.origin);
console.log(window.location.href);
keycloakService.login({
redirectUri: `${window.location.origin}${segments['url']}`,
async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
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 (!this.authenticated && !authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
// return false;
}
// Get the user Keycloak roles and the required from the route
const roles: string[] = keycloakService.getUserRoles(true);
const requiredRoles = route.data?.['roles'];
// 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
// Allow the user to proceed if no additional roles are required to access the route.
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
return true;
}
const authorized = requiredRoles.every(role => roles.includes(role));
if (authorized) {
return true;
// Allow the user to proceed if all the required roles are present.
return requiredRoles.every(role => this.roles.includes(role));
}
}
return router.createUrlTree(['/home']);
};

View File

@ -1,77 +1,95 @@
import { Injectable, inject } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptorFn,
HttpHandlerFn,
} from '@angular/common/http';
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, combineLatest, from, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { KeycloakService } from '../services/keycloak.service';
import { ExcludedUrlRegex } from '../models/keycloak-options';
import { KeycloakService } from '../services/keycloak.service';
export const keycloakBearerInterceptor: HttpInterceptorFn = (req, next) => {
//return next(req);
const keycloak = inject(KeycloakService);
const { enableBearerInterceptor, excludedUrls } = keycloak;
if (!enableBearerInterceptor) {
return next(req);
}
/**
* This interceptor includes the bearer by default in all HttpClient requests.
*
* If you need to exclude some URLs from adding the bearer, please, take a look
* at the {@link KeycloakOptions} bearerExcludedUrls property.
*/
@Injectable()
export class KeycloakBearerInterceptor implements HttpInterceptor {
constructor(private keycloak: KeycloakService) {}
const shallPass: boolean =
!keycloak.shouldAddToken(req) ||
excludedUrls.findIndex((item) => isUrlExcluded(req, item)) > -1;
if (shallPass) {
return next(req);
}
return combineLatest([
from(conditionallyUpdateToken(req)),
of(keycloak.isLoggedIn()),
]).pipe(
mergeMap(([_, isLoggedIn]) =>
isLoggedIn ? handleRequestWithTokenHeader(req, next) : next(req)
)
);
};
function isUrlExcluded(
{ method, url }: HttpRequest<unknown>,
{ urlPattern, httpMethods }: ExcludedUrlRegex
): boolean {
const httpTest =
httpMethods.length === 0 ||
httpMethods.join().indexOf(method.toUpperCase()) > -1;
const urlTest = urlPattern.test(url);
return httpTest && urlTest;
}
function handleRequestWithTokenHeader(
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
return this.keycloak.addTokenToHeader(req.headers).pipe(
mergeMap((headersWithBearer:string) => {
const kcReq = req.clone({
headers: req.headers.set('Authorization', headersWithBearer)
});//req.clone({ headers: headersWithBearer });
return next(kcReq);
})
);
}
async function conditionallyUpdateToken(
req: HttpRequest<unknown>
): Promise<boolean> {
/**
* Calls to update the keycloak token if the request should update the token.
*
* @param req http request from @angular http module.
* @returns
* A promise boolean for the token update or noop result.
*/
private async conditionallyUpdateToken(req: HttpRequest<unknown>): Promise<boolean> {
if (this.keycloak.shouldUpdateToken(req)) {
return await this.keycloak.updateToken();
}
return true;
}
/**
* @deprecated
* Checks if the url is excluded from having the Bearer Authorization
* header added.
*
* @param req http request from @angular http module.
* @param excludedUrlRegex contains the url pattern and the http methods,
* excluded from adding the bearer at the Http Request.
*/
private isUrlExcluded({ method, url }: HttpRequest<unknown>, { urlPattern, httpMethods }: ExcludedUrlRegex): boolean {
const httpTest = httpMethods.length === 0 || httpMethods.join().indexOf(method.toUpperCase()) > -1;
const urlTest = urlPattern.test(url);
return httpTest && urlTest;
}
/**
* Intercept implementation that checks if the request url matches the excludedUrls.
* If not, adds the Authorization header to the request if the user is logged in.
*
* @param req
* @param next
*/
public intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const { enableBearerInterceptor, excludedUrls } = this.keycloak;
if (!enableBearerInterceptor) {
return next.handle(req);
}
const shallPass: boolean = !this.keycloak.shouldAddToken(req) || excludedUrls.findIndex(item => this.isUrlExcluded(req, item)) > -1;
if (shallPass) {
return next.handle(req);
}
return combineLatest([from(this.conditionallyUpdateToken(req)), of(this.keycloak.isLoggedIn())]).pipe(mergeMap(([_, isLoggedIn]) => (isLoggedIn ? this.handleRequestWithTokenHeader(req, next) : next.handle(req))));
}
/**
* Adds the token of the current user to the Authorization header
*
* @param req
* @param next
*/
private handleRequestWithTokenHeader(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return this.keycloak.addTokenToHeader(req.headers).pipe(
mergeMap(headersWithBearer => {
const kcReq = req.clone({ headers: headersWithBearer });
return next.handle(kcReq);
}),
);
}
}

View File

@ -1,64 +0,0 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
/**
* Keycloak event types, as described at the keycloak-js documentation:
* https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events
*/
export enum KeycloakEventType {
/**
* Called if there was an error during authentication.
*/
OnAuthError,
/**
* Called if the user is logged out
* (will only be called if the session status iframe is enabled, or in Cordova mode).
*/
OnAuthLogout,
/**
* Called if there was an error while trying to refresh the token.
*/
OnAuthRefreshError,
/**
* Called when the token is refreshed.
*/
OnAuthRefreshSuccess,
/**
* Called when a user is successfully authenticated.
*/
OnAuthSuccess,
/**
* Called when the adapter is initialized.
*/
OnReady,
/**
* Called when the access token is expired. If a refresh token is available the token
* can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow)
* you can redirect to login screen to obtain a new access token.
*/
OnTokenExpired,
/**
* Called when a AIA has been requested by the application.
*/
OnActionUpdate,
}
/**
* Structure of an event triggered by Keycloak, contains it's type
* and arguments (if any).
*/
export interface KeycloakEvent {
/**
* Event type as described at {@link KeycloakEventType}.
*/
type: KeycloakEventType;
/**
* Arguments from the keycloak-js event function.
*/
args?: unknown;
}

View File

@ -1,135 +0,0 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { HttpRequest } from '@angular/common/http';
/**
* HTTP Methods
*/
export type HttpMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
/**
* ExcludedUrl type may be used to specify the url and the HTTP method that
* should not be intercepted by the KeycloakBearerInterceptor.
*
* Example:
* const excludedUrl: ExcludedUrl[] = [
* {
* url: 'reports/public'
* httpMethods: ['GET']
* }
* ]
*
* In the example above for URL reports/public and HTTP Method GET the
* bearer will not be automatically added.
*
* If the url is informed but httpMethod is undefined, then the bearer
* will not be added for all HTTP Methods.
*/
export interface ExcludedUrl {
url: string;
httpMethods?: HttpMethods[];
}
/**
* Similar to ExcludedUrl, contains the HTTP methods and a regex to
* include the url patterns.
* This interface is used internally by the KeycloakService.
*/
export interface ExcludedUrlRegex {
urlPattern: RegExp;
httpMethods?: HttpMethods[];
}
/**
* keycloak-angular initialization options.
*/
export interface KeycloakOptions {
/**
* Configs to init the keycloak-js library. If undefined, will look for a keycloak.json file
* at root of the project.
* If not undefined, can be a string meaning the url to the keycloak.json file or an object
* of {@link Keycloak.KeycloakConfig}. Use this configuration if you want to specify the keycloak server,
* realm, clientId. This is usefull if you have different configurations for production, stage
* and development environments. Hint: Make use of Angular environment configuration.
*/
config?: string | Keycloak.KeycloakConfig;
/**
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
*/
initOptions?: Keycloak.KeycloakInitOptions;
/**
* By default all requests made by Angular HttpClient will be intercepted in order to
* add the bearer in the Authorization Http Header. However, if this is a not desired
* feature, the enableBearerInterceptor must be false.
*
* Briefly, if enableBearerInterceptor === false, the bearer will not be added
* to the authorization header.
*
* The default value is true.
*/
enableBearerInterceptor?: boolean;
/**
* Forces the execution of loadUserProfile after the keycloak initialization considering that the
* user logged in.
* This option is recommended if is desirable to have the user details at the beginning,
* so after the login, the loadUserProfile function will be called and its value cached.
*
* The default value is true.
*/
loadUserProfileAtStartUp?: boolean;
/**
* @deprecated
* String Array to exclude the urls that should not have the Authorization Header automatically
* added. This library makes use of Angular Http Interceptor, to automatically add the Bearer
* token to the request.
*/
bearerExcludedUrls?: (string | ExcludedUrl)[];
/**
* This value will be used as the Authorization Http Header name. The default value is
* **Authorization**. If the backend expects requests to have a token in a different header, you
* should change this value, i.e: **JWT-Authorization**. This will result in a Http Header
* Authorization as "JWT-Authorization: bearer <token>".
*/
authorizationHeaderName?: string;
/**
* This value will be included in the Authorization Http Header param. The default value is
* **Bearer**, which will result in a Http Header Authorization as "Authorization: Bearer <token>".
*
* If any other value is needed by the backend in the authorization header, you should change this
* value.
*
* Warning: this value must be in compliance with the keycloak server instance and the adapter.
*/
bearerPrefix?: string;
/**
* This value will be used to determine whether or not the token needs to be updated. If the token
* will expire is fewer seconds than the updateMinValidity value, then it will be updated.
*
* The default value is 20.
*/
updateMinValidity?: number;
/**
* A function that will tell the KeycloakBearerInterceptor whether to add the token to the request
* or to leave the request as it is. If the returned value is `true`, the request will have the token
* present on it. If it is `false`, the token will be left off the request.
*
* The default is a function that always returns `true`.
*/
shouldAddToken?: (request: HttpRequest<unknown>) => boolean;
/**
* A function that will tell the KeycloakBearerInterceptor if the token should be considered for
* updating as a part of the request being made. If the returned value is `true`, the request will
* check the token's expiry time and if it is less than the number of seconds configured by
* updateMinValidity then it will be updated before the request is made. If the returned value is
* false, the token will not be updated.
*
* The default is a function that always returns `true`.
*/
shouldUpdateToken?: (request: HttpRequest<unknown>) => boolean;
}

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
@ -9,7 +10,6 @@ import { BusinessListing, User } from '../../../../../../bizmatch-server/src/mod
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
@ -9,7 +10,6 @@ import { CommercialPropertyListing, User } from '../../../../../../bizmatch-serv
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { Observable } from 'rxjs';
@ -9,7 +10,6 @@ import { KeycloakUser, ListingCriteria } from '../../../../../../bizmatch-server
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
@ -9,7 +10,6 @@ import { DropdownModule } from 'primeng/dropdown';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { KeycloakService } from '../../services/keycloak.service';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../utils/utils';

View File

@ -4,11 +4,11 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons';
import { KeycloakService } from 'keycloak-angular';
import { ButtonModule } from 'primeng/button';
import { DividerModule } from 'primeng/divider';
import { RippleModule } from 'primeng/ripple';
import { StyleClassModule } from 'primeng/styleclass';
import { KeycloakService } from '../../services/keycloak.service';
@Component({
selector: 'menu-account',

View File

@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { KeycloakService } from '../../services/keycloak.service';
import { KeycloakService } from 'keycloak-angular';
import { SharedModule } from '../../shared/shared/shared.module';
@Component({

View File

@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { KeycloakService } from 'keycloak-angular';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
@ -17,7 +18,6 @@ import { environment } from '../../../../environments/environment';
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { LoadingService } from '../../../services/loading.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { SubscriptionsService } from '../../../services/subscriptions.service';

View File

@ -9,6 +9,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { KeycloakService } from 'keycloak-angular';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
@ -23,7 +24,6 @@ import { InputNumberModule } from '../../../components/inputnumber/inputnumber.c
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';

View File

@ -10,6 +10,7 @@ import { HttpEventType } from '@angular/common/http';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { KeycloakService } from 'keycloak-angular';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
@ -26,7 +27,6 @@ import { InputNumberModule } from '../../../components/inputnumber/inputnumber.c
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { KeycloakService } from '../../../services/keycloak.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakUser, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { KeycloakService } from '../../../services/keycloak.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { SharedModule } from '../../../shared/shared/shared.module';

View File

@ -1,8 +1,8 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { KeycloakService } from '../../../services/keycloak.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';

View File

@ -1,20 +1,18 @@
import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../environments/environment';
import { createLogger } from '../utils/utils';
import { KeycloakService } from './keycloak.service';
const logger = createLogger('KeycloakInitializerService');
@Injectable({ providedIn: 'root' })
export class KeycloakInitializerService {
private initialized = false;
public initialized = false;
constructor(private keycloakService: KeycloakService) {}
async initialize(): Promise<void> {
if (this.initialized) {
return;
}
const authenticated = await this.keycloakService.init({
async initialize(): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
try {
await this.keycloakService.init({
config: {
url: environment.keycloak.url,
realm: environment.keycloak.realm,
@ -23,15 +21,44 @@ export class KeycloakInitializerService {
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
// flow: 'implicit',
},
});
logger.info(`--->${authenticated}`);
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 {
return this.initialized;
}
// isInitialized(): boolean {
// return this.initialized;
// }
}

View File

@ -1,541 +0,0 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { HttpHeaders, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import Keycloak from 'keycloak-js';
import { Subject, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { KeycloakEvent, KeycloakEventType } from '../models/keycloak-event';
import { ExcludedUrl, ExcludedUrlRegex, KeycloakOptions } from '../models/keycloak-options';
/**
* Service to expose existent methods from the Keycloak JS adapter, adding new
* functionalities to improve the use of keycloak in Angular v > 4.3 applications.
*
* This class should be injected in the application bootstrap, so the same instance will be used
* along the web application.
*/
@Injectable()
export class KeycloakService {
/**
* Keycloak-js instance.
*/
private _instance: Keycloak.KeycloakInstance;
/**
* User profile as KeycloakProfile interface.
*/
private _userProfile: Keycloak.KeycloakProfile;
/**
* Flag to indicate if the bearer will not be added to the authorization header.
*/
private _enableBearerInterceptor: boolean;
/**
* When the implicit flow is choosen there must exist a silentRefresh, as there is
* no refresh token.
*/
private _silentRefresh: boolean;
/**
* Indicates that the user profile should be loaded at the keycloak initialization,
* just after the login.
*/
private _loadUserProfileAtStartUp: boolean;
/**
* The bearer prefix that will be appended to the Authorization Header.
*/
private _bearerPrefix: string;
/**
* Value that will be used as the Authorization Http Header name.
*/
private _authorizationHeaderName: string;
/**
* @deprecated
* The excluded urls patterns that must skip the KeycloakBearerInterceptor.
*/
private _excludedUrls: ExcludedUrlRegex[];
/**
* Observer for the keycloak events
*/
private _keycloakEvents$: Subject<KeycloakEvent> = new Subject<KeycloakEvent>();
/**
* The amount of required time remaining before expiry of the token before the token will be refreshed.
*/
private _updateMinValidity: number;
/**
* Returns true if the request should have the token added to the headers by the KeycloakBearerInterceptor.
*/
shouldAddToken: (request: HttpRequest<unknown>) => boolean;
/**
* Returns true if the request being made should potentially update the token.
*/
shouldUpdateToken: (request: HttpRequest<unknown>) => boolean;
/**
* Binds the keycloak-js events to the keycloakEvents Subject
* which is a good way to monitor for changes, if needed.
*
* The keycloakEvents returns the keycloak-js event type and any
* argument if the source function provides any.
*/
private bindsKeycloakEvents(): void {
this._instance.onAuthError = errorData => {
this._keycloakEvents$.next({
args: errorData,
type: KeycloakEventType.OnAuthError,
});
};
this._instance.onAuthLogout = () => {
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthLogout });
};
this._instance.onAuthRefreshSuccess = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnAuthRefreshSuccess,
});
};
this._instance.onAuthRefreshError = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnAuthRefreshError,
});
};
this._instance.onAuthSuccess = () => {
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthSuccess });
};
this._instance.onTokenExpired = () => {
this._keycloakEvents$.next({
type: KeycloakEventType.OnTokenExpired,
});
};
this._instance.onActionUpdate = state => {
this._keycloakEvents$.next({
args: state,
type: KeycloakEventType.OnActionUpdate,
});
};
this._instance.onReady = authenticated => {
this._keycloakEvents$.next({
args: authenticated,
type: KeycloakEventType.OnReady,
});
};
}
/**
* Loads all bearerExcludedUrl content in a uniform type: ExcludedUrl,
* so it becomes easier to handle.
*
* @param bearerExcludedUrls array of strings or ExcludedUrl that includes
* the url and HttpMethod.
*/
private loadExcludedUrls(bearerExcludedUrls: (string | ExcludedUrl)[]): ExcludedUrlRegex[] {
const excludedUrls: ExcludedUrlRegex[] = [];
for (const item of bearerExcludedUrls) {
let excludedUrl: ExcludedUrlRegex;
if (typeof item === 'string') {
excludedUrl = { urlPattern: new RegExp(item, 'i'), httpMethods: [] };
} else {
excludedUrl = {
urlPattern: new RegExp(item.url, 'i'),
httpMethods: item.httpMethods,
};
}
excludedUrls.push(excludedUrl);
}
return excludedUrls;
}
/**
* Handles the class values initialization.
*
* @param options
*/
private initServiceValues({
enableBearerInterceptor = true,
loadUserProfileAtStartUp = false,
bearerExcludedUrls = [],
authorizationHeaderName = 'Authorization',
bearerPrefix = 'Bearer',
initOptions,
updateMinValidity = 20,
shouldAddToken = () => true,
shouldUpdateToken = () => true,
}: KeycloakOptions): void {
this._enableBearerInterceptor = enableBearerInterceptor;
this._loadUserProfileAtStartUp = loadUserProfileAtStartUp;
this._authorizationHeaderName = authorizationHeaderName;
this._bearerPrefix = bearerPrefix.trim().concat(' ');
this._excludedUrls = this.loadExcludedUrls(bearerExcludedUrls);
this._silentRefresh = initOptions ? initOptions.flow === 'implicit' : false;
this._updateMinValidity = updateMinValidity;
this.shouldAddToken = shouldAddToken;
this.shouldUpdateToken = shouldUpdateToken;
}
/**
* Keycloak initialization. It should be called to initialize the adapter.
* Options is an object with 2 main parameters: config and initOptions. The first one
* will be used to create the Keycloak instance. The second one are options to initialize the
* keycloak instance.
*
* @param options
* Config: may be a string representing the keycloak URI or an object with the
* following content:
* - url: Keycloak json URL
* - realm: realm name
* - clientId: client id
*
* initOptions:
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
*
* enableBearerInterceptor:
* Flag to indicate if the bearer will added to the authorization header.
*
* loadUserProfileInStartUp:
* Indicates that the user profile should be loaded at the keycloak initialization,
* just after the login.
*
* bearerExcludedUrls:
* String Array to exclude the urls that should not have the Authorization Header automatically
* added.
*
* authorizationHeaderName:
* This value will be used as the Authorization Http Header name.
*
* bearerPrefix:
* This value will be included in the Authorization Http Header param.
*
* tokenUpdateExcludedHeaders:
* Array of Http Header key/value maps that should not trigger the token to be updated.
*
* updateMinValidity:
* This value determines if the token will be refreshed based on its expiration time.
*
* @returns
* A Promise with a boolean indicating if the initialization was successful.
*/
public async init(options: KeycloakOptions = {}) {
this.initServiceValues(options);
const { config, initOptions } = options;
this._instance = new Keycloak(config);
this.bindsKeycloakEvents();
const authenticated = await this._instance.init(initOptions);
if (authenticated && this._loadUserProfileAtStartUp) {
await this.loadUserProfile();
}
return authenticated;
}
/**
* Redirects to login form on (options is an optional object with redirectUri and/or
* prompt fields).
*
* @param options
* Object, where:
* - redirectUri: Specifies the uri to redirect to after login.
* - prompt:By default the login screen is displayed if the user is not logged-in to Keycloak.
* To only authenticate to the application if the user is already logged-in and not display the
* login page if the user is not logged-in, set this option to none. To always require
* re-authentication and ignore SSO, set this option to login .
* - maxAge: Used just if user is already authenticated. Specifies maximum time since the
* authentication of user happened. If user is already authenticated for longer time than
* maxAge, the SSO is ignored and he will need to re-authenticate again.
* - loginHint: Used to pre-fill the username/email field on the login form.
* - action: If value is 'register' then user is redirected to registration page, otherwise to
* login page.
* - locale: Specifies the desired locale for the UI.
* @returns
* A void Promise if the login is successful and after the user profile loading.
*/
public async login(options: Keycloak.KeycloakLoginOptions = {}) {
await this._instance.login(options);
if (this._loadUserProfileAtStartUp) {
await this.loadUserProfile();
}
}
/**
* Redirects to logout.
*
* @param redirectUri
* Specifies the uri to redirect to after logout.
* @returns
* A void Promise if the logout was successful, cleaning also the userProfile.
*/
public async logout(redirectUri?: string) {
const options = {
redirectUri,
};
await this._instance.logout(options);
this._userProfile = undefined;
}
/**
* Redirects to registration form. Shortcut for login with option
* action = 'register'. Options are same as for the login method but 'action' is set to
* 'register'.
*
* @param options
* login options
* @returns
* A void Promise if the register flow was successful.
*/
public async register(options: Keycloak.KeycloakLoginOptions = { action: 'register' }) {
await this._instance.register(options);
}
/**
* Check if the user has access to the specified role. It will look for roles in
* realm and the given resource, but will not check if the user is logged in for better performance.
*
* @param role
* role name
* @param resource
* resource name. If not specified, `clientId` is used
* @returns
* A boolean meaning if the user has the specified Role.
*/
isUserInRole(role: string, resource?: string): boolean {
let hasRole: boolean;
hasRole = this._instance.hasResourceRole(role, resource);
if (!hasRole) {
hasRole = this._instance.hasRealmRole(role);
}
return hasRole;
}
/**
* Return the roles of the logged user. The realmRoles parameter, with default value
* true, will return the resource roles and realm roles associated with the logged user. If set to false
* it will only return the resource roles. The resource parameter, if specified, will return only resource roles
* associated with the given resource.
*
* @param realmRoles
* Set to false to exclude realm roles (only client roles)
* @param resource
* resource name If not specified, returns roles from all resources
* @returns
* Array of Roles associated with the logged user.
*/
getUserRoles(realmRoles: boolean = true, resource?: string): string[] {
let roles: string[] = [];
if (this._instance.resourceAccess) {
Object.keys(this._instance.resourceAccess).forEach(key => {
if (resource && resource !== key) {
return;
}
const resourceAccess = this._instance.resourceAccess[key];
const clientRoles = resourceAccess['roles'] || [];
roles = roles.concat(clientRoles);
});
}
if (realmRoles && this._instance.realmAccess) {
const realmRoles = this._instance.realmAccess['roles'] || [];
roles.push(...realmRoles);
}
return roles;
}
/**
* Check if user is logged in.
*
* @returns
* A boolean that indicates if the user is logged in.
*/
isLoggedIn(): boolean {
if (!this._instance) {
return false;
}
return this._instance.authenticated;
}
/**
* Returns true if the token has less than minValidity seconds left before
* it expires.
*
* @param minValidity
* Seconds left. (minValidity) is optional. Default value is 0.
* @returns
* Boolean indicating if the token is expired.
*/
isTokenExpired(minValidity: number = 0): boolean {
return this._instance.isTokenExpired(minValidity);
}
/**
* If the token expires within _updateMinValidity seconds the token is refreshed. If the
* session status iframe is enabled, the session status is also checked.
* Returns a promise telling if the token was refreshed or not. If the session is not active
* anymore, the promise is rejected.
*
* @param minValidity
* Seconds left. (minValidity is optional, if not specified updateMinValidity - default 20 is used)
* @returns
* Promise with a boolean indicating if the token was succesfully updated.
*/
public async updateToken(minValidity = this._updateMinValidity) {
// TODO: this is a workaround until the silent refresh (issue #43)
// is not implemented, avoiding the redirect loop.
if (this._silentRefresh) {
if (this.isTokenExpired()) {
throw new Error('Failed to refresh the token, or the session is expired');
}
return true;
}
if (!this._instance) {
throw new Error('Keycloak Angular library is not initialized.');
}
try {
return await this._instance.updateToken(minValidity);
} catch (error) {
return false;
}
}
/**
* Loads the user profile.
* Returns promise to set functions to be invoked if the profile was loaded
* successfully, or if the profile could not be loaded.
*
* @param forceReload
* If true will force the loadUserProfile even if its already loaded.
* @returns
* A promise with the KeycloakProfile data loaded.
*/
public async loadUserProfile(forceReload = false) {
if (this._userProfile && !forceReload) {
return this._userProfile;
}
if (!this._instance.authenticated) {
throw new Error('The user profile was not loaded as the user is not logged in.');
}
return (this._userProfile = await this._instance.loadUserProfile());
}
/**
* Returns the authenticated token.
*/
public async getToken() {
return this._instance.token;
}
/**
* Returns the logged username.
*
* @returns
* The logged username.
*/
public getUsername() {
if (!this._userProfile) {
throw new Error('User not logged in or user profile was not loaded.');
}
return this._userProfile.username;
}
/**
* Clear authentication state, including tokens. This can be useful if application
* has detected the session was expired, for example if updating token fails.
* Invoking this results in onAuthLogout callback listener being invoked.
*/
clearToken(): void {
this._instance.clearToken();
}
/**
* Adds a valid token in header. The key & value format is:
* Authorization Bearer <token>.
* If the headers param is undefined it will create the Angular headers object.
*
* @param headers
* Updated header with Authorization and Keycloak token.
* @returns
* An observable with with the HTTP Authorization header and the current token.
*/
public addTokenToHeader(headers: HttpHeaders = new HttpHeaders()) {
return from(this.getToken()).pipe(map(token => (token ? headers.set(this._authorizationHeaderName, this._bearerPrefix + token) : headers)));
}
/**
* Returns the original Keycloak instance, if you need any customization that
* this Angular service does not support yet. Use with caution.
*
* @returns
* The KeycloakInstance from keycloak-js.
*/
getKeycloakInstance(): Keycloak.KeycloakInstance {
return this._instance;
}
/**
* @deprecated
* Returns the excluded URLs that should not be considered by
* the http interceptor which automatically adds the authorization header in the Http Request.
*
* @returns
* The excluded urls that must not be intercepted by the KeycloakBearerInterceptor.
*/
get excludedUrls(): ExcludedUrlRegex[] {
return this._excludedUrls;
}
/**
* Flag to indicate if the bearer will be added to the authorization header.
*
* @returns
* Returns if the bearer interceptor was set to be disabled.
*/
get enableBearerInterceptor(): boolean {
return this._enableBearerInterceptor;
}
/**
* Keycloak subject to monitor the events triggered by keycloak-js.
* The following events as available (as described at keycloak docs -
* https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events):
* - OnAuthError
* - OnAuthLogout
* - OnAuthRefreshError
* - OnAuthRefreshSuccess
* - OnAuthSuccess
* - OnReady
* - OnTokenExpire
* In each occurrence of any of these, this subject will return the event type,
* described at {@link KeycloakEventType} enum and the function args from the keycloak-js
* if provided any.
*
* @returns
* A subject with the {@link KeycloakEvent} which describes the event type and attaches the
* function args.
*/
get keycloakEvents$(): Subject<KeycloakEvent> {
return this._keycloakEvents$;
}
}

View File

@ -7,5 +7,6 @@ export const environment_base = {
url: 'https://auth.bizmatch.net',
realm: 'bizmatch-dev',
clientId: 'bizmatch-dev',
redirectUri: 'https://dev.bizmatch.net',
},
};

View File

@ -5,3 +5,4 @@ environment.mailinfoUrl = 'http://localhost:4200';
environment.imageBaseUrl = 'http://localhost:4200';
environment.keycloak.clientId = 'dev';
environment.keycloak.realm = 'dev';
environment.keycloak.redirectUri = 'http://localhost:4200';

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;
},
};
}