Compare commits
No commits in common. "d6768b3da976c309af936a3b15343a4d9630bebc" and "214327031c2aa2a9b608c0748f2c700b4ea9a3ed" have entirely different histories.
d6768b3da9
...
214327031c
|
|
@ -37,7 +37,6 @@
|
|||
"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",
|
||||
|
|
@ -67,4 +66,4 @@
|
|||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
|
@ -11,6 +10,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@ 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()),
|
||||
|
|
@ -20,10 +17,9 @@ export const appConfig: ApplicationConfig = {
|
|||
{
|
||||
provide: APP_INITIALIZER,
|
||||
// useFactory: initializeKeycloak,
|
||||
//useFactory: initializeKeycloak,
|
||||
useFactory: initializeKeycloak3,
|
||||
useFactory: (keycloakInitializer: KeycloakInitializerService) => async () => await keycloakInitializer.initialize(),
|
||||
multi: true,
|
||||
//deps: [KeycloakService],
|
||||
// deps: [KeycloakService],
|
||||
deps: [KeycloakInitializerService],
|
||||
},
|
||||
{
|
||||
|
|
@ -53,32 +49,9 @@ 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,
|
||||
|
|
@ -90,6 +63,6 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
},
|
||||
});
|
||||
logger.info(`+++>${authenticated}`);
|
||||
console.log(`--->${authenticated}`);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
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';
|
||||
|
|
@ -65,62 +64,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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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';
|
||||
|
|
@ -11,6 +10,7 @@ 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',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { KeycloakService } from '../../services/keycloak.service';
|
||||
|
||||
@Component({
|
||||
selector: 'logout',
|
||||
|
|
|
|||
|
|
@ -1,41 +1,42 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
|
||||
import { inject } from '@angular/core';
|
||||
import { CanMatchFn, Router, UrlTree } from '@angular/router';
|
||||
|
||||
// Services
|
||||
import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
|
||||
import { KeycloakService } from '../services/keycloak.service';
|
||||
import { createLogger } from '../utils/utils';
|
||||
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);
|
||||
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();
|
||||
}
|
||||
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.
|
||||
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));
|
||||
// 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;
|
||||
}
|
||||
|
||||
return router.createUrlTree(['/home']);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,95 +1,77 @@
|
|||
/**
|
||||
* @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 { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
HttpHandler,
|
||||
HttpEvent,
|
||||
HttpInterceptorFn,
|
||||
HttpHandlerFn,
|
||||
} from '@angular/common/http';
|
||||
|
||||
import { Observable, combineLatest, from, of } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { ExcludedUrlRegex } from '../models/keycloak-options';
|
||||
import { KeycloakService } from '../services/keycloak.service';
|
||||
import { ExcludedUrlRegex } from '../models/keycloak-options';
|
||||
|
||||
/**
|
||||
* 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) {}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
export const keycloakBearerInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
//return next(req);
|
||||
const keycloak = inject(KeycloakService);
|
||||
const { enableBearerInterceptor, excludedUrls } = keycloak;
|
||||
if (!enableBearerInterceptor) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
const shallPass: boolean =
|
||||
!keycloak.shouldAddToken(req) ||
|
||||
excludedUrls.findIndex((item) => isUrlExcluded(req, item)) > -1;
|
||||
if (shallPass) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
return combineLatest([
|
||||
from(conditionallyUpdateToken(req)),
|
||||
of(keycloak.isLoggedIn()),
|
||||
]).pipe(
|
||||
mergeMap(([_, isLoggedIn]) =>
|
||||
isLoggedIn ? handleRequestWithTokenHeader(req, next) : next(req)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const shallPass: boolean = !this.keycloak.shouldAddToken(req) || excludedUrls.findIndex(item => this.isUrlExcluded(req, item)) > -1;
|
||||
if (shallPass) {
|
||||
return next.handle(req);
|
||||
}
|
||||
function isUrlExcluded(
|
||||
{ method, url }: HttpRequest<unknown>,
|
||||
{ urlPattern, httpMethods }: ExcludedUrlRegex
|
||||
): boolean {
|
||||
const httpTest =
|
||||
httpMethods.length === 0 ||
|
||||
httpMethods.join().indexOf(method.toUpperCase()) > -1;
|
||||
|
||||
return combineLatest([from(this.conditionallyUpdateToken(req)), of(this.keycloak.isLoggedIn())]).pipe(mergeMap(([_, isLoggedIn]) => (isLoggedIn ? this.handleRequestWithTokenHeader(req, next) : next.handle(req))));
|
||||
}
|
||||
const urlTest = urlPattern.test(url);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
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> {
|
||||
if (this.keycloak.shouldUpdateToken(req)) {
|
||||
return await this.keycloak.updateToken();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
|
@ -10,6 +9,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
|
@ -10,6 +9,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
|
@ -10,6 +9,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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';
|
||||
|
|
@ -10,6 +9,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { KeycloakService } from '../../services/keycloak.service';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
|
||||
@Component({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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';
|
||||
|
|
@ -18,6 +17,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ 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';
|
||||
|
|
@ -24,6 +23,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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';
|
||||
|
|
@ -27,6 +26,7 @@ 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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,64 +1,37 @@
|
|||
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 {
|
||||
public initialized = false;
|
||||
private initialized = false;
|
||||
|
||||
constructor(private keycloakService: KeycloakService) {}
|
||||
|
||||
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,
|
||||
clientId: environment.keycloak.clientId,
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
// flow: 'implicit',
|
||||
},
|
||||
});
|
||||
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}`);
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.initialized = true;
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`--->${authenticated}`);
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// isInitialized(): boolean {
|
||||
// return this.initialized;
|
||||
// }
|
||||
isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,541 @@
|
|||
/**
|
||||
* @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$;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,5 @@ export const environment_base = {
|
|||
url: 'https://auth.bizmatch.net',
|
||||
realm: 'bizmatch-dev',
|
||||
clientId: 'bizmatch-dev',
|
||||
redirectUri: 'https://dev.bizmatch.net',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,4 +5,3 @@ environment.mailinfoUrl = 'http://localhost:4200';
|
|||
environment.imageBaseUrl = 'http://localhost:4200';
|
||||
environment.keycloak.clientId = 'dev';
|
||||
environment.keycloak.realm = 'dev';
|
||||
environment.keycloak.redirectUri = 'http://localhost:4200';
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue