keycloak-angular
Version:
Easy Keycloak integration for Angular applications.
1,228 lines (1,213 loc) • 66.8 kB
JavaScript
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',