diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index 25d2c2c..6e83032 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -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 { KeycloakAuthGuard } 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: [KeycloakAuthGuard], }, { path: 'account/:id', component: AccountComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // Create, Update Listings { path: 'editBusinessListing/:id', component: EditBusinessListingComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, { path: 'createBusinessListing', component: EditBusinessListingComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, { path: 'editCommercialPropertyListing/:id', component: EditCommercialPropertyListingComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, { path: 'createCommercialPropertyListing', component: EditCommercialPropertyListingComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // My Listings { path: 'myListings', component: MyListingComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // My Favorites { path: 'myFavorites', component: FavoritesComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // EMAil Us { path: 'emailUs', component: EmailUsComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // Logout { path: 'logout', component: LogoutComponent, - canActivate: [authGuard], + canActivate: [KeycloakAuthGuard], }, // ######### // Pricing diff --git a/bizmatch/src/app/guards/auth.guard.ts b/bizmatch/src/app/guards/auth.guard.ts index 12bfbb5..920829c 100644 --- a/bizmatch/src/app/guards/auth.guard.ts +++ b/bizmatch/src/app/guards/auth.guard.ts @@ -1,42 +1,105 @@ -import { inject } from '@angular/core'; -import { CanMatchFn, Router, UrlTree } from '@angular/router'; +// import { inject } from '@angular/core'; +// import { CanMatchFn, Router, UrlTree } from '@angular/router'; -// Services -import { KeycloakInitializerService } from '../services/keycloak-initializer.service'; +// // Services +// import { KeycloakInitializerService } from '../services/keycloak-initializer.service'; +// import { KeycloakService } from '../services/keycloak.service'; import { KeycloakService } from '../services/keycloak.service'; import { createLogger } from '../utils/utils'; const logger = createLogger('authGuard'); -export const authGuard: CanMatchFn = async (route, segments): Promise => { - 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']}`, - }); +// export const authGuard: CanMatchFn = async (route, segments): Promise => { +// 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']}`, +// }); +// } + +// // Get the user Keycloak roles and the required from the route +// const roles: string[] = keycloakService.getUserRoles(true); +// 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; +// } + +// const authorized = requiredRoles.every(role => roles.includes(role)); + +// if (authorized) { +// return true; +// } + +// return router.createUrlTree(['/home']); +// }; +/** + * @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 { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; + +/** + * A simple guard implementation out of the box. This class should be inherited and + * implemented by the application. The only method that should be implemented is #isAccessAllowed. + * The reason for this is that the authorization flow is usually not unique, so in this way you will + * have more freedom to customize your authorization flow. + */ +export class KeycloakAuthGuard implements CanActivate { + /** + * Indicates if the user is authenticated or not. + */ + protected authenticated: boolean; + /** + * Roles of the logged user. It contains the clientId and realm user roles. + */ + protected roles: string[]; + + constructor(protected router: Router, protected keycloakAngular: KeycloakService) {} + + /** + * CanActivate checks if the user is logged in and get the full list of roles (REALM + CLIENT) + * of the logged user. This values are set to authenticated and roles params. + * + * @param route + * @param state + */ + async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + try { + this.authenticated = await this.keycloakAngular.isLoggedIn(); + this.roles = await this.keycloakAngular.getUserRoles(true); + + return await this.isAccessAllowed(route, state); + } catch (error) { + throw new Error('An error happened during access validation. Details:' + error); + } } - // Get the user Keycloak roles and the required from the route - const roles: string[] = keycloakService.getUserRoles(true); - 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; + /** + * Create your own customized authorization flow in this method. From here you already known + * if the user is authenticated (this.authenticated) and the user roles (this.roles). + * + * Return a UrlTree if the user should be redirected to another route. + * + * @param route + * @param state + */ + async isAccessAllowed(route, state): Promise { + if (!this.authenticated) { + await this.router.navigate(['/home']); + } + return this.authenticated; } - - const authorized = requiredRoles.every(role => roles.includes(role)); - - if (authorized) { - return true; - } - - return router.createUrlTree(['/home']); -}; +} diff --git a/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts b/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts index e0fa1eb..2e64ad3 100644 --- a/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts +++ b/bizmatch/src/app/interceptors/keycloak-bearer.interceptor.ts @@ -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) {} + + /** + * 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): Promise { + if (this.keycloak.shouldUpdateToken(req)) { + return await this.keycloak.updateToken(); + } + + return true; } - const shallPass: boolean = - !keycloak.shouldAddToken(req) || - excludedUrls.findIndex((item) => isUrlExcluded(req, item)) > -1; - if (shallPass) { - 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, { urlPattern, httpMethods }: ExcludedUrlRegex): boolean { + const httpTest = httpMethods.length === 0 || httpMethods.join().indexOf(method.toUpperCase()) > -1; + + const urlTest = urlPattern.test(url); + + return httpTest && urlTest; } - return combineLatest([ - from(conditionallyUpdateToken(req)), - of(keycloak.isLoggedIn()), - ]).pipe( - mergeMap(([_, isLoggedIn]) => - isLoggedIn ? handleRequestWithTokenHeader(req, next) : 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, next: HttpHandler): Observable> { + const { enableBearerInterceptor, excludedUrls } = this.keycloak; + if (!enableBearerInterceptor) { + return next.handle(req); + } -function isUrlExcluded( - { method, url }: HttpRequest, - { urlPattern, httpMethods }: ExcludedUrlRegex -): boolean { - const httpTest = - httpMethods.length === 0 || - httpMethods.join().indexOf(method.toUpperCase()) > -1; + const shallPass: boolean = !this.keycloak.shouldAddToken(req) || excludedUrls.findIndex(item => this.isUrlExcluded(req, item)) > -1; + if (shallPass) { + return next.handle(req); + } - const urlTest = urlPattern.test(url); + return combineLatest([from(this.conditionallyUpdateToken(req)), of(this.keycloak.isLoggedIn())]).pipe(mergeMap(([_, isLoggedIn]) => (isLoggedIn ? this.handleRequestWithTokenHeader(req, next) : next.handle(req)))); + } - return httpTest && urlTest; + /** + * Adds the token of the current user to the Authorization header + * + * @param req + * @param next + */ + private handleRequestWithTokenHeader(req: HttpRequest, next: HttpHandler): Observable> { + return this.keycloak.addTokenToHeader(req.headers).pipe( + mergeMap(headersWithBearer => { + const kcReq = req.clone({ headers: headersWithBearer }); + return next.handle(kcReq); + }), + ); + } } - -function handleRequestWithTokenHeader( - req: HttpRequest, - next: HttpHandlerFn -): Observable> { - 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 -): Promise { - if (this.keycloak.shouldUpdateToken(req)) { - return await this.keycloak.updateToken(); - } - - return true; -} \ No newline at end of file diff --git a/bizmatch/src/app/services/keycloak-initializer.service.ts b/bizmatch/src/app/services/keycloak-initializer.service.ts index 70900bf..4df1332 100644 --- a/bizmatch/src/app/services/keycloak-initializer.service.ts +++ b/bizmatch/src/app/services/keycloak-initializer.service.ts @@ -24,6 +24,11 @@ export class KeycloakInitializerService { onLoad: 'check-sso', silentCheckSsoRedirectUri: (window).location.origin + '/assets/silent-check-sso.html', }, + // initOptions: { + // pkceMethod: 'S256', + // redirectUri: environment.keycloak.redirectUri, + // checkLoginIframe: false, + // }, }); logger.info(`--->${authenticated}`); diff --git a/bizmatch/src/environments/environment.base.ts b/bizmatch/src/environments/environment.base.ts index a3d04c3..cb992b5 100644 --- a/bizmatch/src/environments/environment.base.ts +++ b/bizmatch/src/environments/environment.base.ts @@ -7,5 +7,6 @@ export const environment_base = { url: 'https://auth.bizmatch.net', realm: 'bizmatch-dev', clientId: 'bizmatch-dev', + redirectUri: 'https://dev.bizmatch.net', }, }; diff --git a/bizmatch/src/environments/environment.ts b/bizmatch/src/environments/environment.ts index 130a7ca..5c0076c 100644 --- a/bizmatch/src/environments/environment.ts +++ b/bizmatch/src/environments/environment.ts @@ -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';