UNPKG

keycloak-angular

Version:

Easy Keycloak integration for Angular applications.

1,228 lines (1,213 loc) 66.8 kB
import * as i0 from '@angular/core'; import { Injectable, inject, NgModule, signal, InjectionToken, TemplateRef, ViewContainerRef, effect, Input, Directive, NgZone, computed, PLATFORM_ID, provideAppInitializer, EnvironmentInjector, runInInjectionContext, makeEnvironmentProviders } from '@angular/core'; import { HttpHeaders, HTTP_INTERCEPTORS } from '@angular/common/http'; import { Subject, from, combineLatest, of, fromEvent, mergeMap as mergeMap$1 } from 'rxjs'; import { map, mergeMap, debounceTime, takeUntil } from 'rxjs/operators'; import Keycloak from 'keycloak-js'; import { CommonModule, isPlatformBrowser } from '@angular/common'; /** * @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 * * @deprecated Keycloak Event based on the KeycloakService is deprecated and * will be removed in future versions. * Use the new `KEYCLOAK_EVENT_SIGNAL` injection token to listen for the keycloak * events. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ var KeycloakEventTypeLegacy; (function (KeycloakEventTypeLegacy) { /** * Called if there was an error during authentication. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnAuthError"] = 0] = "OnAuthError"; /** * Called if the user is logged out * (will only be called if the session status iframe is enabled, or in Cordova mode). */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnAuthLogout"] = 1] = "OnAuthLogout"; /** * Called if there was an error while trying to refresh the token. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnAuthRefreshError"] = 2] = "OnAuthRefreshError"; /** * Called when the token is refreshed. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnAuthRefreshSuccess"] = 3] = "OnAuthRefreshSuccess"; /** * Called when a user is successfully authenticated. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnAuthSuccess"] = 4] = "OnAuthSuccess"; /** * Called when the adapter is initialized. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnReady"] = 5] = "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. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnTokenExpired"] = 6] = "OnTokenExpired"; /** * Called when a AIA has been requested by the application. */ KeycloakEventTypeLegacy[KeycloakEventTypeLegacy["OnActionUpdate"] = 7] = "OnActionUpdate"; })(KeycloakEventTypeLegacy || (KeycloakEventTypeLegacy = {})); /** * @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 */ /** * 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. * * @deprecated Class based guards are deprecated in Keycloak Angular and will be removed in future versions. * Use the new `createAuthGuard` function to create a Guard for your application. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ class KeycloakAuthGuard { constructor(router, keycloakAngular) { this.router = router; this.keycloakAngular = keycloakAngular; } /** * 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, state) { 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); } } } /** * @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 */ /** * 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. * * @deprecated This service is deprecated and will be removed in future versions. * Use the new `provideKeycloak` function to load Keycloak in an Angular application. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ class KeycloakService { constructor() { /** * Observer for the keycloak events */ this._keycloakEvents$ = new Subject(); } /** * 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. */ bindsKeycloakEvents() { this._instance.onAuthError = (errorData) => { this._keycloakEvents$.next({ args: errorData, type: KeycloakEventTypeLegacy.OnAuthError }); }; this._instance.onAuthLogout = () => { this._keycloakEvents$.next({ type: KeycloakEventTypeLegacy.OnAuthLogout }); }; this._instance.onAuthRefreshSuccess = () => { this._keycloakEvents$.next({ type: KeycloakEventTypeLegacy.OnAuthRefreshSuccess }); }; this._instance.onAuthRefreshError = () => { this._keycloakEvents$.next({ type: KeycloakEventTypeLegacy.OnAuthRefreshError }); }; this._instance.onAuthSuccess = () => { this._keycloakEvents$.next({ type: KeycloakEventTypeLegacy.OnAuthSuccess }); }; this._instance.onTokenExpired = () => { this._keycloakEvents$.next({ type: KeycloakEventTypeLegacy.OnTokenExpired }); }; this._instance.onActionUpdate = (state) => { this._keycloakEvents$.next({ args: state, type: KeycloakEventTypeLegacy.OnActionUpdate }); }; this._instance.onReady = (authenticated) => { this._keycloakEvents$.next({ args: authenticated, type: KeycloakEventTypeLegacy.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. */ loadExcludedUrls(bearerExcludedUrls) { const excludedUrls = []; for (const item of bearerExcludedUrls) { let excludedUrl; 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 */ initServiceValues({ enableBearerInterceptor = true, loadUserProfileAtStartUp = false, bearerExcludedUrls = [], authorizationHeaderName = 'Authorization', bearerPrefix = 'Bearer', initOptions, updateMinValidity = 20, shouldAddToken = () => true, shouldUpdateToken = () => true }) { 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. */ async init(options = {}) { 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. */ async login(options = {}) { 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. */ async logout(redirectUri) { 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. */ async register(options = { 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, resource) { let hasRole; 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 = true, resource) { let roles = []; 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() { 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 = 0) { 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. */ 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. */ 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. */ async getToken() { return this._instance.token; } /** * Returns the logged username. * * @returns * The logged username. */ 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() { 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. */ addTokenToHeader(headers = 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() { 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() { 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() { 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 KeycloakEventTypeLegacy} enum and the function args from the keycloak-js * if provided any. * * @returns * A subject with the {@link KeycloakEventLegacy} which describes the event type and attaches the * function args. */ get keycloakEvents$() { return this._keycloakEvents$; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakService, decorators: [{ type: Injectable }] }); /** * @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 */ /** * 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. * * @deprecated KeycloakBearerInterceptor is deprecated and will be removed in future versions. * Use the new functional interceptor such as `includeBearerTokenInterceptor`. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ class KeycloakBearerInterceptor { constructor() { this.keycloak = inject(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. */ async conditionallyUpdateToken(req) { 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. */ isUrlExcluded({ method, url }, { urlPattern, httpMethods }) { 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 */ intercept(req, next) { const { enableBearerInterceptor, excludedUrls } = this.keycloak; if (!enableBearerInterceptor) { return next.handle(req); } const shallPass = !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 */ handleRequestWithTokenHeader(req, next) { return this.keycloak.addTokenToHeader(req.headers).pipe(mergeMap((headersWithBearer) => { const kcReq = req.clone({ headers: headersWithBearer }); return next.handle(kcReq); })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakBearerInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakBearerInterceptor }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakBearerInterceptor, decorators: [{ type: Injectable }] }); /** * @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 */ /** * @deprecated NgModules are deprecated in Keycloak Angular and will be removed in future versions. * Use the new `provideKeycloak` function to load Keycloak in an Angular application. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ class CoreModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: CoreModule, imports: [CommonModule] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CoreModule, providers: [ KeycloakService, { provide: HTTP_INTERCEPTORS, useClass: KeycloakBearerInterceptor, multi: true } ], imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CoreModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule], providers: [ KeycloakService, { provide: HTTP_INTERCEPTORS, useClass: KeycloakBearerInterceptor, multi: true } ] }] }] }); /** * @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 */ /** * @deprecated NgModules are deprecated in Keycloak Angular and will be removed in future versions. * Use the new `provideKeycloak` function to load Keycloak in an Angular application. * More info: https://github.com/mauriciovigolo/keycloak-angular/blob/main/docs/migration-guides/v19.md */ class KeycloakAngularModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakAngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: KeycloakAngularModule, imports: [CoreModule] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakAngularModule, imports: [CoreModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: KeycloakAngularModule, decorators: [{ type: NgModule, args: [{ imports: [CoreModule] }] }] }); /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ // This legacy implementation will be removed in Keycloak Angular v20 /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ var KeycloakEventType; (function (KeycloakEventType) { /** * Keycloak Angular is not initialized yet. This is the initial state applied to the Keycloak Event Signal. * Note: This event is only emitted in Keycloak Angular, it is not part of the keycloak-js. */ KeycloakEventType["KeycloakAngularNotInitialized"] = "KeycloakAngularNotInitialized"; /** * Keycloak Angular is in the process of initializing the providers and Keycloak Instance. * Note: This event is only emitted in Keycloak Angular, it is not part of the keycloak-js. */ KeycloakEventType["KeycloakAngularInit"] = "KeycloakAngularInit"; /** * Triggered if there is an error during authentication. */ KeycloakEventType["AuthError"] = "AuthError"; /** * Triggered when the user logs out. This event will only be triggered * if the session status iframe is enabled or in Cordova mode. */ KeycloakEventType["AuthLogout"] = "AuthLogout"; /** * Triggered if an error occurs while attempting to refresh the token. */ KeycloakEventType["AuthRefreshError"] = "AuthRefreshError"; /** * Triggered when the token is successfully refreshed. */ KeycloakEventType["AuthRefreshSuccess"] = "AuthRefreshSuccess"; /** * Triggered when a user is successfully authenticated. */ KeycloakEventType["AuthSuccess"] = "AuthSuccess"; /** * Triggered when the Keycloak adapter has completed initialization. */ KeycloakEventType["Ready"] = "Ready"; /** * Triggered when the access token expires. Depending on the flow, you may * need to use `updateToken` to refresh the token or redirect the user * to the login screen. */ KeycloakEventType["TokenExpired"] = "TokenExpired"; /** * Triggered when an authentication action is requested by the application. */ KeycloakEventType["ActionUpdate"] = "ActionUpdate"; })(KeycloakEventType || (KeycloakEventType = {})); /** * Helper function to typecast unknown arguments into a specific Keycloak event type. * * @template T - The expected argument type. * @param args - The arguments to be cast. * @returns The arguments typed as `T`. */ const typeEventArgs = (args) => args; /** * Creates a signal to manage Keycloak events, initializing the signal with * appropriate default values or values from a given Keycloak instance. * * @param keycloak - An instance of the Keycloak client. * @returns A `Signal` that tracks the current Keycloak event state. */ const createKeycloakSignal = (keycloak) => { const keycloakSignal = signal({ type: KeycloakEventType.KeycloakAngularInit }); if (!keycloak) { keycloakSignal.set({ type: KeycloakEventType.KeycloakAngularNotInitialized }); return keycloakSignal; } keycloak.onReady = (authenticated) => { keycloakSignal.set({ type: KeycloakEventType.Ready, args: authenticated }); }; keycloak.onAuthError = (errorData) => { keycloakSignal.set({ type: KeycloakEventType.AuthError, args: errorData }); }; keycloak.onAuthLogout = () => { keycloakSignal.set({ type: KeycloakEventType.AuthLogout }); }; keycloak.onActionUpdate = (status, action) => { keycloakSignal.set({ type: KeycloakEventType.ActionUpdate, args: { status, action } }); }; keycloak.onAuthRefreshError = () => { keycloakSignal.set({ type: KeycloakEventType.AuthRefreshError }); }; keycloak.onAuthRefreshSuccess = () => { keycloakSignal.set({ type: KeycloakEventType.AuthRefreshSuccess }); }; keycloak.onAuthSuccess = () => { keycloakSignal.set({ type: KeycloakEventType.AuthSuccess }); }; keycloak.onTokenExpired = () => { keycloakSignal.set({ type: KeycloakEventType.TokenExpired }); }; return keycloakSignal; }; /** * Injection token for the Keycloak events signal, used for dependency injection. */ const KEYCLOAK_EVENT_SIGNAL = new InjectionToken('Keycloak Events Signal'); /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ /** * Structural directive to conditionally display elements based on Keycloak user roles. * * This directive checks if the authenticated user has at least one of the specified roles. * Roles can be validated against a specific **resource (client ID)** or the **realm**. * * ### Features: * - Supports role checking in both **resources (client-level roles)** and the **realm**. * - Accepts an array of roles to match. * - Optional configuration to check realm-level roles. * * ### Inputs: * - `kaHasRoles` (Required): Array of roles to validate. * - `resource` (Optional): The client ID or resource name to validate resource-level roles. * - `checkRealm` (Optional): A boolean flag to enable realm role validation (default is `false`). * * ### Requirements: * - A Keycloak instance must be injected via Angular's dependency injection. * - The user must be authenticated in Keycloak. * * @example * #### Example 1: Check for Global Realm Roles * Show the content only if the user has the `admin` or `editor` role in the realm. * ```html * <div *kaHasRoles="['admin', 'editor']; checkRealm:true"> * <p>This content is visible only to users with 'admin' or 'editor' realm roles.</p> * </div> * ``` * * @example * #### Example 2: Check for Resource Roles * Show the content only if the user has the `read` or `write` role for a specific resource (`my-client`). * ```html * <div *kaHasRoles="['read', 'write']; resource:'my-client'"> * <p>This content is visible only to users with 'read' or 'write' roles for 'my-client'.</p> * </div> * ``` * * @example * #### Example 3: Check for Both Resource and Realm Roles * Show the content if the user has the roles in either the realm or a resource. * ```html * <div *kaHasRoles="['admin', 'write']; resource:'my-client' checkRealm:true"> * <p>This content is visible to users with 'admin' in the realm or 'write' in 'my-client'.</p> * </div> * ``` * * @example * #### Example 4: Fallback Content When Roles Do Not Match * Use an `<ng-template>` to display fallback content if the user lacks the required roles. * ```html * <div *kaHasRoles="['admin']; resource:'my-client'"> * <p>Welcome, Admin!</p> * </div> * <ng-template #noAccess> * <p>Access Denied</p> * </ng-template> * ``` */ class HasRolesDirective { constructor() { this.templateRef = inject(TemplateRef); this.viewContainer = inject(ViewContainerRef); this.keycloak = inject(Keycloak); /** * List of roles to validate against the resource or realm. */ this.roles = []; /** * Flag to enable realm-level role validation. */ this.checkRealm = false; this.viewContainer.clear(); const keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL); effect(() => { const keycloakEvent = keycloakSignal(); if (keycloakEvent.type !== KeycloakEventType.Ready) { return; } const authenticated = typeEventArgs(keycloakEvent.args); if (authenticated) { this.render(); } }); } render() { const hasAccess = this.checkUserRoles(); if (hasAccess) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } } /** * Checks if the user has at least one of the specified roles in the resource or realm. * @returns True if the user has access, false otherwise. */ checkUserRoles() { const hasResourceRole = this.roles.some((role) => this.keycloak.hasResourceRole(role, this.resource)); const hasRealmRole = this.checkRealm ? this.roles.some((role) => this.keycloak.hasRealmRole(role)) : false; return hasResourceRole || hasRealmRole; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: HasRolesDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: HasRolesDirective, isStandalone: true, selector: "[kaHasRoles]", inputs: { roles: ["kaHasRoles", "roles"], resource: ["kaHasRolesResource", "resource"], checkRealm: ["kaHasRolesCheckRealm", "checkRealm"] }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: HasRolesDirective, decorators: [{ type: Directive, args: [{ selector: '[kaHasRoles]' }] }], ctorParameters: () => [], propDecorators: { roles: [{ type: Input, args: ['kaHasRoles'] }], resource: [{ type: Input, args: ['kaHasRolesResource'] }], checkRealm: [{ type: Input, args: ['kaHasRolesCheckRealm'] }] } }); /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ /** * Service to monitor user activity in an Angular application. * Tracks user interactions (e.g., mouse movement, touch, key presses, clicks, and scrolls) * and updates the last activity timestamp. Consumers can check for user inactivity * based on a configurable timeout. * * The service is supposed to be used in the client context and for safety, it checks during the startup * if it is a browser context. */ class UserActivityService { constructor() { this.ngZone = inject(NgZone); /** * Signal to store the timestamp of the last user activity. * The timestamp is represented as the number of milliseconds since epoch. */ this.lastActivity = signal(Date.now()); /** * Subject to signal the destruction of the service. * Used to clean up RxJS subscriptions. */ this.destroy$ = new Subject(); /** * Computed signal to expose the last user activity as a read-only signal. */ this.lastActivitySignal = computed(() => this.lastActivity()); } /** * Starts monitoring user activity events (`mousemove`, `touchstart`, `keydown`, `click`, `scroll`) * and updates the last activity timestamp using RxJS with debounce. * The events are processed outside Angular zone for performance optimization. */ startMonitoring() { const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); if (!isBrowser) { return; } this.ngZone.runOutsideAngular(() => { const events = ['mousemove', 'touchstart', 'keydown', 'click', 'scroll']; events.forEach((event) => { fromEvent(window, event) .pipe(debounceTime(300), takeUntil(this.destroy$)) .subscribe(() => this.updateLastActivity()); }); }); } /** * Updates the last activity timestamp to the current time. * This method runs inside Angular's zone to ensure reactivity with Angular signals. */ updateLastActivity() { this.ngZone.run(() => { this.lastActivity.set(Date.now()); }); } /** * Retrieves the timestamp of the last recorded user activity. * @returns {number} The last activity timestamp in milliseconds since epoch. */ get lastActivityTime() { return this.lastActivity(); } /** * Determines whether the user interacted with the application, meaning it is activily using the application, based on * the specified duration. * @param timeout - The inactivity timeout in milliseconds. * @returns {boolean} `true` if the user is inactive, otherwise `false`. */ isActive(timeout) { return Date.now() - this.lastActivityTime < timeout; } /** * Cleans up RxJS subscriptions and resources when the service is destroyed. * This method is automatically called by Angular when the service is removed. */ ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: UserActivityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: UserActivityService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: UserActivityService, decorators: [{ type: Injectable }] }); /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ /** * Service to automatically manage the Keycloak token refresh process * based on user activity and token expiration events. This service * integrates with Keycloak for session management and interacts with * user activity monitoring to determine the appropriate action when * the token expires. * * The service listens to `KeycloakSignal` for token-related events * (e.g., `TokenExpired`) and provides configurable options for * session timeout and inactivity handling. */ class AutoRefreshTokenService { constructor() { this.keycloak = inject(Keycloak); this.userActivity = inject(UserActivityService); this.options = this.defaultOptions; this.initialized = false; const keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL); effect(() => { const keycloakEvent = keycloakSignal(); if (keycloakEvent.type === KeycloakEventType.TokenExpired) { this.processTokenExpiredEvent(); } }); } get defaultOptions() { return { sessionTimeout: 300000, onInactivityTimeout: 'logout' }; } executeOnInactivityTimeout() { switch (this.options.onInactivityTimeout) { case 'login': this.keycloak.login().catch((error) => console.error('Failed to execute the login call', error)); break; case 'logout': this.keycloak.logout().catch((error) => console.error('Failed to execute the logout call', error)); break; default: break; } } processTokenExpiredEvent() { if (!this.initialized || !this.keycloak.authenticated) { return; } if (this.userActivity.isActive(this.options.sessionTimeout)) { this.keycloak.updateToken().catch(() => this.executeOnInactivityTimeout()); } else { this.executeOnInactivityTimeout(); } } start(options) { this.options = { ...this.defaultOptions, ...options }; this.initialized = true; this.userActivity.startMonitoring(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AutoRefreshTokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AutoRefreshTokenService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AutoRefreshTokenService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ /** * Enables automatic token refresh and session inactivity handling for a * Keycloak-enabled Angular application. * * This function initializes a service that tracks user interactions, such as * mouse movements, touches, key presses, clicks, and scrolls. If user activity * is detected, it periodically calls `Keycloak.updateToken` to ensure the bearer * token remains valid and does not expire. * * If the session remains inactive beyond the defined `sessionTimeout`, the * specified action (`logout`, `login`, or `none`) will be executed. By default, * the service will call `keycloak.logout` upon inactivity timeout. * * Event tracking uses RxJS observables with a debounce of 300 milliseconds to * monitor user interactions. When the Keycloak `OnTokenExpired` event occurs, * the service checks the user's last activity timestamp. If the user has been * active within the session timeout period, it refreshes the token using `updateToken`. * * * @param options - Configuration options for the auto-refresh token feature. * - `sessionTimeout` (optional): The duration in milliseconds after which * the session is considered inactive. Defaults to `300000` (5 minutes). * - `onInactivityTimeout` (optional): The action to take when session inactivity * exceeds the specified timeout. Defaults to `'logout'`. * - `'login'`: Execute `keycloak.login` function. * - `'logout'`: Logs the user out by calling `keycloak.logout`. * - `'none'`: No action is taken. * * @returns A `KeycloakFeature` instance that configures and enables the * auto-refresh token functionality. */ function withAutoRefreshToken(options) { return { configure: () => { const autoRefreshTokenService = inject(AutoRefreshTokenService); autoRefreshTokenService.start(options); } }; } /** * @license * Copyright Mauricio Gemelli Vigolo All Rights Reserved. * * 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 */ const mapResourceRoles = (resourceAccess = {}) => { return Object.entries(resourceAccess).reduce((roles, [key, value]) => { roles[key] = value.roles; return roles; }, {}); }; /** * Creates a custom authorization guard for Angular routes, enabling fine-grained access control. * * This guard invokes the provided `isAccessAllowed` function to determine if access is permitted * based on the current route, router state, and user's authentication and roles data. * * @template T - The type of the guard function (`CanActivateFn` or `CanActivateChildFn`). * @param isAccessAllowed - A callback function that evaluates access conditions. The function receives: * - `route`: The current `ActivatedRouteSnapshot` for the route being accessed. * - `state`: The current `RouterStateSnapshot` representing the router's state. * - `authData`: An `AuthGuardData` object containing the user's authentication status, roles, and Keycloak instance. * @returns A guard function of type `T` that can be used as a route `canActivate` or `canActivateChild` guard. * * @example * ```ts * import { createAuthGuard } from './auth-guard'; * import { Routes } from '@angular/router'; * * const isUserAllowed = async (route, state, authData) => { * const { authenticated, grantedRoles } = authData; * return authenticated && grantedRoles.realmRoles.includes('admin'); * }; * * const routes: Routes = [ * { * path: 'admin',