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