@ebondu/angular2-keycloak
Version:
1,210 lines (1,198 loc) • 76.4 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, NgModule, InjectionToken, inject, Injector, PLATFORM_ID, NgZone } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders, HttpErrorResponse, HttpEventType } from '@angular/common/http';
import { BehaviorSubject, Observable, of, EMPTY, throwError } from 'rxjs';
import { v4 } from 'uuid';
import { fromByteArray } from 'base64-js';
import { sha256 } from 'js-sha256';
import { filter, map, mergeMap, switchMap, tap, first, catchError, finalize } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common';
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class AngularKeycloakService {
constructor() {
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class AngularKeycloakModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakModule });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: AngularKeycloakModule, decorators: [{
type: NgModule,
args: [{
imports: []
}]
}] });
/*
* Copyright 2022 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const KEYCLOAK_JSON_PATH = new InjectionToken('keycloakJsonPath');
const KEYCLOAK_INIT_OPTIONS = new InjectionToken('keycloakOptions');
const KEYCLOAK_CONF = new InjectionToken('keycloakConfiguration');
var KeycloakAdapterName;
(function (KeycloakAdapterName) {
KeycloakAdapterName["CORDOVA"] = "cordova";
KeycloakAdapterName["DEFAULT"] = "default";
KeycloakAdapterName["ANY"] = "any";
})(KeycloakAdapterName || (KeycloakAdapterName = {}));
var KeycloakOnLoad;
(function (KeycloakOnLoad) {
KeycloakOnLoad["LOGIN_REQUIRED"] = "login-required";
KeycloakOnLoad["CHECK_SSO"] = "check-sso";
})(KeycloakOnLoad || (KeycloakOnLoad = {}));
var KeycloakResponseMode;
(function (KeycloakResponseMode) {
KeycloakResponseMode["QUERY"] = "query";
KeycloakResponseMode["FRAGMENT"] = "fragment";
})(KeycloakResponseMode || (KeycloakResponseMode = {}));
var KeycloakResponseType;
(function (KeycloakResponseType) {
KeycloakResponseType["CODE"] = "code";
KeycloakResponseType["ID_TOKEN"] = "id_token token";
KeycloakResponseType["CODE_ID_TOKEN"] = "code id_token token";
})(KeycloakResponseType || (KeycloakResponseType = {}));
var KeycloakFlow;
(function (KeycloakFlow) {
KeycloakFlow["STANDARD"] = "standard";
KeycloakFlow["IMPLICIT"] = "implicit";
KeycloakFlow["HYBRID"] = "hybrid";
})(KeycloakFlow || (KeycloakFlow = {}));
var LogoutMethod;
(function (LogoutMethod) {
LogoutMethod["POST"] = "post";
LogoutMethod["GET"] = "get";
})(LogoutMethod || (LogoutMethod = {}));
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Default adapter for web browsers
*/
class DefaultAdapter {
keycloak;
constructor(keycloak) {
this.keycloak = keycloak;
}
login(options) {
window.location.href = this.keycloak.createLoginUrl(options);
}
logout(options) {
window.location.href = this.keycloak.createLogoutUrl(options);
}
register(options) {
window.location.href = this.keycloak.createRegisterUrl(options);
}
accountManagement() {
window.location.href = this.keycloak.createAccountUrl({});
}
passwordManagement() {
window.location.href = this.keycloak.createChangePasswordUrl({});
}
redirectUri(options, encodeHash) {
if (arguments.length === 1) {
encodeHash = true;
}
if (options && options.redirectUri) {
return options.redirectUri;
}
else {
let redirectUri = location.href;
if (location.hash && encodeHash) {
redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
redirectUri += (redirectUri.indexOf('?') === -1 ? '?' : '&') + 'redirect_fragment=' +
encodeURIComponent(location.hash.substring(1));
}
return redirectUri;
}
}
}
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* To store Keycloak objects like tokens using a localStorage.
*/
class LocalStorage {
clearExpired() {
const time = new Date().getTime();
for (let i = 1; i <= localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.indexOf('kc-callback-') === 0) {
const value = localStorage.getItem(key);
if (value) {
try {
const expires = JSON.parse(value).expires;
if (!expires || expires < time) {
localStorage.removeItem(key);
}
}
catch (err) {
localStorage.removeItem(key);
}
}
}
}
}
get(state) {
if (!state) {
return;
}
const key = 'kc-callback-' + state;
let value = localStorage.getItem(key);
if (value) {
localStorage.removeItem(key);
value = JSON.parse(value);
}
this.clearExpired();
return value;
}
add(state) {
this.clearExpired();
const key = 'kc-callback-' + state.state;
state.expires = new Date().getTime() + (60 * 60 * 1000);
localStorage.setItem(key, JSON.stringify(state));
}
}
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* URI parser.
*/
class URIParser {
static initialParse(uriToParse, responseMode) {
let baseUri;
let queryString;
let fragmentString;
const questionMarkIndex = uriToParse.indexOf('?');
let fragmentIndex = uriToParse.indexOf('#', questionMarkIndex + 1);
if (questionMarkIndex === -1 && fragmentIndex === -1) {
baseUri = uriToParse;
}
else if (questionMarkIndex !== -1) {
baseUri = uriToParse.substring(0, questionMarkIndex);
queryString = uriToParse.substring(questionMarkIndex + 1);
if (fragmentIndex !== -1) {
fragmentIndex = queryString.indexOf('#');
fragmentString = queryString.substring(fragmentIndex + 1);
queryString = queryString.substring(0, fragmentIndex);
}
}
else {
baseUri = uriToParse.substring(0, fragmentIndex);
fragmentString = uriToParse.substring(fragmentIndex + 1);
}
return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString };
}
static parseParams(paramString) {
const result = {};
const params = paramString.split('&');
for (let i = 0; i < params.length; i++) {
const p = params[i].split('=');
const paramName = decodeURIComponent(p[0]);
const paramValue = decodeURIComponent(p[1]);
result[paramName] = paramValue;
}
return result;
}
static handleQueryParam(paramName, paramValue, oauth) {
const supportedOAuthParams = ['code', 'state', 'error', 'session_state', 'error_description'];
for (let i = 0; i < supportedOAuthParams.length; i++) {
if (paramName === supportedOAuthParams[i]) {
oauth[paramName] = paramValue;
return true;
}
}
return false;
}
static parseUri(uriToParse, responseMode) {
const parsedUri = this.initialParse(decodeURIComponent(uriToParse), responseMode);
let queryParams = {};
if (parsedUri.queryString) {
queryParams = this.parseParams(parsedUri.queryString);
}
const oauth = { newUrl: parsedUri.baseUri };
Object.keys(queryParams).forEach(param => {
switch (param) {
case 'redirect_fragment':
oauth.fragment = queryParams[param];
break;
case 'prompt':
oauth.prompt = queryParams[param];
break;
default:
if (responseMode !== 'query' || !this.handleQueryParam(param, queryParams[param], oauth)) {
oauth.newUrl += (oauth.newUrl.indexOf('?') === -1 ? '?' : '&') + param + '=' + queryParams[param];
}
break;
}
});
if (responseMode === 'fragment') {
let fragmentParams = {};
if (parsedUri.fragmentString) {
fragmentParams = this.parseParams(parsedUri.fragmentString);
}
Object.keys(fragmentParams).forEach(param => {
oauth[param] = fragmentParams[param];
});
}
return oauth;
}
}
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Token utility
*/
class Token {
static decodeToken(str) {
str = str.split('.')[1];
str = str.replace('/-/g', '+');
str = str.replace('/_/g', '/');
switch (str.length % 4) {
case 0:
break;
case 2:
str += '==';
break;
case 3:
str += '=';
break;
default:
throw new Error('Invalid token');
}
str = (str + '===').slice(0, str.length + (str.length % 4));
str = str.replace(/-/g, '+').replace(/_/g, '/');
str = decodeURIComponent(escape(atob(str)));
str = JSON.parse(str);
return str;
}
static generateRandomData(len) {
// use web crypto APIs if possible
let array = null;
const crypto = window.crypto;
if (crypto && crypto.getRandomValues && window.Uint8Array) {
array = new Uint8Array(len);
crypto.getRandomValues(array);
return array;
}
// fallback to Math random
array = new Array(len);
for (let j = 0; j < array.length; j++) {
array[j] = Math.floor(256 * Math.random());
}
return array;
}
static generateCodeVerifier(len) {
return Token.generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
}
static generateRandomString(len, alphabet) {
const randomData = this.generateRandomData(len);
const chars = new Array(len);
for (let i = 0; i < len; i++) {
chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);
}
return String.fromCharCode.apply(null, chars);
}
static generatePkceChallenge(pkceMethod, codeVerifier) {
switch (pkceMethod) {
// The use of the "plain" method is considered insecure and therefore not supported.
case 'S256':
// hash codeVerifier, then encode as url-safe base64 without padding
const hashBytes = new Uint8Array(sha256.arrayBuffer(codeVerifier));
const encodedHash = fromByteArray(hashBytes)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/\=/g, '');
return encodedHash;
default:
throw new Error('Invalid value for pkceMethod');
}
}
}
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Cordova adapter for hybrid apps.
*/
class CordovaAdapter {
keycloak;
constructor(keycloak) {
this.keycloak = keycloak;
}
login(options) {
// let promise = Keycloak.createPromise();
let o = 'location=no';
if (options && options.prompt === 'none') {
o += ',hidden=yes';
}
const loginUrl = this.keycloak.createLoginUrl(options);
// console.log('opening login frame from cordova: ' + loginUrl);
if (!window.cordova) {
throw new Error('Cannot authenticate via a web browser');
}
if (!window.cordova.InAppBrowser || !window.cordova.plugins.browsertab) {
throw new Error('The Apache Cordova InAppBrowser/BrowserTab plugins was not found and are required');
}
let ref;
// let ref = window.cordova.InAppBrowser.open(loginUrl, '_blank', o);
// let ref = window.cordova.InAppBrowser.open(loginUrl, '_system', o);
let completed = false;
window.cordova.plugins.browsertab.themeable.isAvailable(function (result) {
if (!result) {
ref = window.cordova.InAppBrowser.open(loginUrl, '_system');
ref.addEventListener('loadstart', function (event) {
if (event.url.indexOf('http://localhost') === 0) {
const callback = this.keycloak.parseCallback(event.url);
this.keycloak.processCallback(callback).subscribe(processed => {
ref.close();
completed = true;
});
}
});
ref.addEventListener('loaderror', function (event) {
if (!completed) {
if (event.url.indexOf('http://localhost') === 0) {
const callback = this.keycloak.parseCallback(event.url);
this.keycloak.processCallback(callback).subscribe(processed => {
this.closeBrowserTab();
// ref.close();
// completed = true;
});
}
else {
this.closeBrowserTab();
// ref.close();
}
}
});
}
else {
this.openBrowserTab(loginUrl, options);
}
}, function (isAvailableError) {
console.error('failed to query availability of in-app browser tab');
});
}
closeBrowserTab() {
const cordova = window.cordova;
cordova.plugins.browsertab.themeable.close();
// completed = true;
}
logout(options) {
const cordova = window.cordova;
const logoutUrl = this.keycloak.createLogoutUrl(options);
let ref;
let error;
cordova.plugins.browsertab.themeable.isAvailable(function (result) {
if (!result) {
ref = cordova.InAppBrowser.open(logoutUrl, '_system');
ref.addEventListener('loadstart', function (event) {
if (event.url.indexOf('http://localhost') === 0) {
this.ref.close();
this.closeBrowserTab();
}
});
ref.addEventListener('loaderror', function (event) {
if (event.url.indexOf('http://localhost') === 0) {
this.ref.close();
this.closeBrowserTab();
}
else {
error = true;
this.ref.close();
this.closeBrowserTab();
}
});
ref.addEventListener('exit', function (event) {
if (error) {
console.error('listener of in-app browser tab exited due to error', error);
}
else {
this.keycloak.clearToken({});
}
});
}
else {
this.openBrowserTab(logoutUrl, options);
}
}, function (isAvailableError) {
console.error('failed to query availability of in-app browser tab', isAvailableError);
});
}
register(options) {
const registerUrl = this.keycloak.createRegisterUrl({});
window.cordova.plugins.browsertab.themeable.isAvailable(function (result) {
if (!result) {
window.cordova.InAppBrowser.open(registerUrl, '_system');
}
else {
this.openBrowserTab(registerUrl, options);
}
}, function (isAvailableError) {
console.error('failed to query availability of in-app browser tab', isAvailableError);
});
}
accountManagement(options) {
const accountUrl = this.keycloak.createAccountUrl({});
window.cordova.plugins.browsertab.themeable.isAvailable(function (result) {
if (!result) {
window.cordova.InAppBrowser.open(accountUrl, '_system');
}
else {
this.openBrowserTab(accountUrl, options);
}
}, function (isAvailableError) {
console.error('failed to query availability of in-app browser tab', isAvailableError);
});
}
passwordManagement(options) {
const accountUrl = this.keycloak.createChangePasswordUrl({});
window.cordova.plugins.browsertab.themeable.isAvailable(function (result) {
if (!result) {
window.cordova.InAppBrowser.open(accountUrl, '_system');
}
else {
this.openBrowserTab(accountUrl, options);
}
}, function (isAvailableError) {
console.error('failed to query availability of in-app browser tab', isAvailableError);
});
}
redirectUri(options) {
if (options.redirectUri) {
return options.redirectUri;
}
else {
return 'http://localhost';
}
}
openBrowserTab(url, options) {
const cordova = window.cordova;
if (options.toolbarColor) {
cordova.plugins.browsertab.themeable.openUrl(url, options);
}
else {
cordova.plugins.browsertab.themeable.openUrl(url);
}
}
}
/*
* Copyright 2018 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* To store Keycloak objects like tokens using a cookie.
*/
class CookieStorage {
getCookie = function (key) {
const name = key + '=';
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return '';
};
get(state) {
if (!state) {
return;
}
const value = this.getCookie('kc-callback-' + state);
this.setCookie('kc-callback-' + state, '', this.cookieExpiration(-100));
if (value) {
return JSON.parse(value);
}
}
add(state) {
this.setCookie('kc-callback-' + state.state, JSON.stringify(state), this.cookieExpiration(60));
}
removeItem(key) {
this.setCookie(key, '', this.cookieExpiration(-100));
}
cookieExpiration(minutes) {
const exp = new Date();
exp.setTime(exp.getTime() + (minutes * 60 * 1000));
return exp;
}
setCookie(key, value, expirationDate) {
const cookie = key + '=' + value + '; '
+ 'expires=' + expirationDate.toUTCString() + '; ';
document.cookie = cookie;
}
}
/*
* Copyright 2022 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Silent login check Iframe utility
*/
class KeycloakSilentCheckLoginIframe {
keycloak;
iframe;
iframeSrc;
constructor(keycloak, silentRedirectUri) {
this.keycloak = keycloak;
this.iframeSrc = this.keycloak.createLoginUrl({
prompt: 'none',
redirectUri: silentRedirectUri
});
this.initIframe();
}
initIframe() {
this.iframe = document.createElement('iframe');
this.iframe.setAttribute('src', this.iframeSrc);
this.iframe.style.display = 'none';
this.iframe.setAttribute('title', 'keycloak-silent-check-sso');
document.body.appendChild(this.iframe);
window.addEventListener('message', () => this.processSilentLoginCallbackMessage(event), false);
}
processSilentLoginCallbackMessage(event) {
const origin = this.iframeSrc.substring(0, this.iframeSrc.indexOf('/', 8));
// console.log('checking iframe message callback..' + event.data + ' ' + event.origin);
if ((event.origin !== window.location.origin) || (this.iframe.contentWindow !== event.source)) {
// console.log('event is not coming from the iframe, ignoring it');
return;
}
const oauth = this.keycloak.parseCallback(event.data);
if (!!oauth) {
this.keycloak.processCallback(oauth).subscribe(() => console.log('Silent login ended'));
}
document.body.removeChild(this.iframe);
window.removeEventListener('message', () => this.processSilentLoginCallbackMessage(event), false);
}
}
/*
* Copyright 2022 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 3Party cookie Iframe utility
*/
class KeycloakCheck3pCookiesIframe {
keycloak;
iframe;
interval;
iframeSrc;
supportedBS;
supportedObs;
constructor(keycloak) {
this.keycloak = keycloak;
this.iframeSrc = this.keycloak.getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';
this.supportedBS = new BehaviorSubject(null);
this.supportedObs = this.supportedBS.asObservable();
this.initIframe();
}
initIframe() {
this.iframe = document.createElement('iframe');
this.iframe.setAttribute('src', this.iframeSrc);
this.iframe.setAttribute('title', 'keycloak-3p-check-iframe');
this.iframe.style.display = 'none';
document.body.appendChild(this.iframe);
window.addEventListener('message', () => this.process3pCookieCallbackMessage(event), false);
}
process3pCookieCallbackMessage(event) {
// console.log('checking iframe message callback..' + event.data + ' ' + event.origin);
if (this.iframe.contentWindow !== event.source) {
// console.log('event is not coming from the iframe, ignoring it');
return;
}
// console.log('Checking iframe message ' + event.data);
if (event.data !== 'supported' && event.data !== 'unsupported') {
return;
}
if (event.data === 'unsupported') {
console.warn('[KEYCLOAK] 3rd party cookies aren\'t supported by this browser.' +
' checkLoginIframe and silent check-sso are not available.');
this.supportedBS.next(false);
}
else {
this.supportedBS.next(true);
}
document.body.removeChild(this.iframe);
window.removeEventListener('message', () => this.process3pCookieCallbackMessage(event));
}
}
/*
* Copyright 2024 ebondu and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Keycloak core classes to manage tokens with a keycloak server.
*
* Used for login, logout, register, account management, profile.
* Provide Angular Observable objects for initialization, authentication, token expiration.
*
*/
class KeycloakService {
initializedObs;
initializedAuthzObs;
authenticationObs;
tokenExpiredObs;
authenticationErrorObs;
// tokens
accessToken;
tokenParsed;
sessionId;
// Observables
initBS;
initAuthzBS;
authenticationsBS;
tokenExpiredBS;
authenticationErrorBS;
refreshToken;
refreshTokenParsed;
rpt;
idToken;
idTokenParsed;
// keycloak conf
umaConfig;
adapter;
callbackStorage;
responseType;
timeSkew;
tokenTimeoutHandle;
subject;
realmAccess;
resourceAccess;
loginIframe;
#injector = inject(Injector);
#platformId = inject(PLATFORM_ID);
#ngZone = inject(NgZone);
#configUrl = inject(KEYCLOAK_JSON_PATH, { optional: true });
keycloakConfig = inject(KEYCLOAK_CONF, { optional: true });
initOptions = inject(KEYCLOAK_INIT_OPTIONS);
get http() {
return this.#injector.get(HttpClient);
}
constructor() {
this.initBS = new BehaviorSubject(false);
this.initializedObs = this.initBS.asObservable();
this.initAuthzBS = new BehaviorSubject(false);
this.initializedAuthzObs = this.initAuthzBS.asObservable();
this.authenticationsBS = new BehaviorSubject(false);
this.authenticationObs = this.authenticationsBS.asObservable();
this.tokenExpiredBS = new BehaviorSubject(false);
this.tokenExpiredObs = this.tokenExpiredBS.asObservable();
this.authenticationErrorBS = new BehaviorSubject(null);
this.authenticationErrorObs = this.authenticationErrorBS.asObservable();
// console.log('Keycloak service created with init options and configuration file', initOptions, configUrl);
if (!isPlatformBrowser(this.#platformId)) {
// console.log('Keycloak service init only available on browser platform');
this.initBS.next(false);
}
else {
if (!globalThis.isSecureContext) {
console.warn('Keycloak JS must be used in a \'secure context\' to function properly as it relies on browser APIs that are otherwise not available');
}
if (this.#configUrl) {
this.http.get(this.#configUrl).subscribe({
next: (config) => {
this.keycloakConfig = {
authServerUrl: config['auth-server-url'],
realm: config['realm'],
clientId: config['resource'],
clientSecret: (config['credentials'] || {})['secret']
};
// console.log('Conf loaded', this.keycloakConfig);
this.initService();
},
error: () => {
// console.log('Unable to load keycloak.json', error);
this.initBS.next(false);
}
});
}
else if (this.keycloakConfig) {
this.initService();
}
else {
// console.log('Keycloak service init fails : no keycloak.json or configuration provided');
this.initBS.next(false);
}
this.initializedObs.pipe(filter(initialized => !!initialized)).subscribe(next => {
// console.log('Keycloak initialized, initializing authz service', this);
if (next) {
const url = this.keycloakConfig.authServerUrl + '/realms/' + this.keycloakConfig.realm + '/.well-known/uma2-configuration';
this.http.get(url).subscribe({
next: (authz) => {
// console.log('Authz configuration file loaded, continuing authz');
this.umaConfig = authz;
this.initAuthzBS.next(true);
},
error: () => {
// console.log('unable to get uma file', error);
this.initAuthzBS.next(false);
}
});
}
});
}
}
parseCallback(url) {
const oauth = URIParser.parseUri(url, this.initOptions.responseMode);
const state = oauth.state;
const oauthState = this.callbackStorage.get(state);
if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {
oauth.valid = true;
oauth.redirectUri = oauthState.redirectUri;
oauth.storedNonce = oauthState.nonce;
oauth.prompt = oauthState.prompt;
oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
if (oauth.fragment) {
oauth.newUrl += '#' + oauth.fragment;
}
return oauth;
}
}
processCallback(oauth) {
return new Observable((observer) => {
const code = oauth.code;
const error = oauth.error;
const prompt = oauth.prompt;
const timeLocal = new Date().getTime();
if (error) {
const errorData = { error: error, error_description: oauth.error_description };
this.authenticationErrorBS.next(errorData);
if (prompt !== 'none') {
// console.log('error while processing callback');
observer.next(false);
}
return;
}
else if ((this.initOptions.flow !== KeycloakFlow.STANDARD) && (oauth.access_token || oauth.id_token)) {
this.authSuccess(oauth.access_token, null, oauth.id_token, true, timeLocal, oauth);
observer.next(true);
}
if ((this.initOptions.flow !== KeycloakFlow.IMPLICIT) && code) {
let withCredentials = false;
const url = this.getRealmUrl() + '/protocol/openid-connect/token';
let params = new HttpParams();
params = params.set('code', code);
params = params.set('grant_type', 'authorization_code');
let headers = new HttpHeaders();
headers = headers.set('Content-type', 'application/x-www-form-urlencoded');
if (this.keycloakConfig.clientId && this.keycloakConfig.clientSecret) {
headers = headers.set('Authorization', 'Basic ' + btoa(this.keycloakConfig.clientId + ':' + this.keycloakConfig.clientSecret));
withCredentials = true;
}
else {
params = params.set('client_id', this.keycloakConfig.clientId);
}
params = params.set('redirect_uri', oauth.redirectUri);
if (oauth.pkceCodeVerifier) {
params = params.set('code_verifier', oauth.pkceCodeVerifier);
}
const options = { headers: headers, withCredentials: withCredentials };
this.http.post(url, params, options).subscribe({
next: (token) => {
this.authSuccess(token['access_token'], token['refresh_token'], token['id_token'], this.initOptions.flow === KeycloakFlow.STANDARD, timeLocal, oauth);
this.authenticationsBS.next(true);
observer.next(true);
},
error: (errorToken) => {
this.authenticationErrorBS.next({ error: errorToken, error_description: 'unable to get token from server' });
// console.log('Unable to get token', errorToken);
observer.next(false);
}
});
}
});
}
login(options) {
return this.adapter.login(options);
}
// ###################################
// ####### Keycloak methods ######
// ###################################
logout(options) {
return this.adapter.logout(options);
}
updateToken(minValidity) {
minValidity = minValidity || 5;
if (!this.isTokenExpired(minValidity)) {
// console.log('token still valid');
return of(this.accessToken);
}
else {
if (this.isRefreshTokenExpired(5)) {
this.login(this.keycloakConfig);
return EMPTY;
}
else {
// console.log('refreshing token');
let params = new HttpParams();
params = params.set('grant_type', 'refresh_token');
params = params.set('refresh_token', this.refreshToken);
const url = this.getRealmUrl() + '/protocol/openid-connect/token';
let headers = new HttpHeaders({ 'Content-type': 'application/x-www-form-urlencoded' });
let withCredentials = false;
if (this.keycloakConfig.clientId && this.keycloakConfig.clientSecret) {
headers = headers.append('Authorization', 'Basic ' + btoa(this.keycloakConfig.clientId + ': ' + this.keycloakConfig.clientSecret));
withCredentials = true;
}
else {
params = params.set('client_id', this.keycloakConfig.clientId);
}
let timeLocal = new Date().getTime();
return this.http.post(url, params, { headers: headers, withCredentials: withCredentials }).pipe(map((token) => {
timeLocal = (timeLocal + new Date().getTime()) / 2;
this.setToken(token['access_token'], token['refresh_token'], token['id_token'], true);
this.timeSkew = Math.floor(timeLocal / 1000) - this.tokenParsed.iat;
return token['access_token'];
}));
}
}
}
register(options) {
return this.adapter.register(options);
}
accountManagement(options) {
return this.adapter.accountManagement(options);
}
loadChangePassword(options) {
return this.adapter.passwordManagement(options);
}
loadUserProfile() {
// need to refresh token to get account-console as aud
let paramsToSend = new HttpParams();
let headersToSend = new HttpHeaders();
headersToSend = headersToSend.set('Content-type', 'application/x-www-form-urlencoded');
paramsToSend = paramsToSend.set('client_id', this.keycloakConfig.clientId);
paramsToSend = paramsToSend.set('grant_type', 'refresh_token');
paramsToSend = paramsToSend.set('refresh_token', this.refreshToken);
headersToSend = headersToSend.set('Authorization', 'bearer ' + this.accessToken);
const url = this.getRealmUrl() + '/account/';
return this.http.post(this.umaConfig?.token_endpoint, paramsToSend, { withCredentials: false, headers: headersToSend })
.pipe(mergeMap((token) => {
const headers = new HttpHeaders({ 'Authorization': 'bearer ' + token.access_token });
return this.http.get(url, { headers: headers, withCredentials: false });
}));
}
updateUserProfile(profile) {
// need to refresh token to get account-console as aud
let paramsToSend = new HttpParams();
let headersToSend = new HttpHeaders();
headersToSend = headersToSend.set('Content-type', 'application/x-www-form-urlencoded');
paramsToSend = paramsToSend.set('client_id', this.keycloakConfig.clientId);
paramsToSend = paramsToSend.set('grant_type', 'refresh_token');
paramsToSend = paramsToSend.set('refresh_token', this.refreshToken);
headersToSend = headersToSend.set('Authorization', 'bearer ' + this.accessToken);
const url = this.getRealmUrl() + '/account/';
return this.http.post(this.umaConfig?.token_endpoint, paramsToSend, { withCredentials: true, headers: headersToSend })
.pipe(mergeMap((token) => {
const headers = new HttpHeaders({ 'Authorization': 'bearer ' + token.access_token });
return this.http.post(url, profile, { headers: headers, withCredentials: false });
}));
}
createDeleteAccountUrl(options) {
const state = v4();
const nonce = this.initOptions.useNonce ? v4() : null;
// const redirectUri = this.getRealmUrl() + '/account/#/personal-info';
const redirectUri = this.adapter.redirectUri({});
const callback = {
state: state,
nonce: nonce,
redirectUri: redirectUri
};
const action = 'auth';
let url = this.getRealmUrl()
+ '/protocol/openid-connect/' + action
+ '?client_id=' + encodeURIComponent(this.keycloakConfig.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
+ '&response_mode=' + encodeURIComponent(this.initOptions.responseMode)
+ '&response_type=' + encodeURIComponent(this.responseType)
+ '&scope=' + encodeURIComponent('openid')
+ '&kc_action=' + encodeURIComponent('delete_account');
if (options.useNonce) {
url += '&nonce=' + encodeURIComponent(nonce);
}
let codeVerifier;
const pkceMethod = this.initOptions.pkceMethod;
codeVerifier = Token.generateCodeVerifier(96);
callback.pkceCodeVerifier = codeVerifier;
const pkceChallenge = Token.generatePkceChallenge(pkceMethod, codeVerifier);
url += '&code_challenge=' + pkceChallenge;
url += '&code_challenge_method=' + pkceMethod;
this.callbackStorage.add(callback);
return url;
}
createUpdateProfileUrl(options) {
const state = v4();
const nonce = this.initOptions.useNonce ? v4() : null;
const redirectUri = this.adapter.redirectUri({});
const callback = {
state: state,
nonce: nonce,
redirectUri: redirectUri
};
const action = 'auth';
let url = this.getRealmUrl()
+ '/protocol/openid-connect/' + action
+ '?client_id=' + encodeURIComponent(this.keycloakConfig.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
+ '&response_mode=' + encodeURIComponent(this.initOptions.responseMode)
+ '&response_type=' + encodeURIComponent(this.responseType)
+ '&scope=' + encodeURIComponent('openid')
+ '&kc_action=' + encodeURIComponent('UPDATE_PROFILE');
if (options.useNonce) {
url += '&nonce=' + encodeURIComponent(nonce);
}
let codeVerifier;
const pkceMethod = this.initOptions.pkceMethod;
codeVerifier = Token.generateCodeVerifier(96);
callback.pkceCodeVerifier = codeVerifier;
const pkceChallenge = Token.generatePkceChallenge(pkceMethod, codeVerifier);
url += '&code_challenge=' + pkceChallenge;
url += '&code_challenge_method=' + pkceMethod;
this.callbackStorage.add(callback);
return url;
}
changePassword() {
const state = v4();
const nonce = this.initOptions.useNonce ? v4() : null;
const redirectUri = this.adapter.redirectUri({});
const callback = {
state: state,
nonce: nonce,
redirectUri: redirectUri
};
const action = 'auth';
let url = this.getRealmUrl()
+ '/protocol/openid-connect/' + action
+ '?client_id=' + encodeURIComponent(this.keycloakConfig.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
+ '&response_mode=' + encodeURIComponent(this.initOptions.responseMode)
+ '&response_type=' + encodeURIComponent(this.responseType)
+ '&scope=' + encodeURIComponent('openid')
+ '&kc_action=' + encodeURIComponent('UPDATE_PASSWORD');
if (this.initOptions.useNonce) {
url += '&nonce=' + encodeURIComponent(nonce);
}
let codeVerifier;
const pkceMethod = this.initOptions.pkceMethod;
codeVerifier = Token.generateCodeVerifier(96);
callback.pkceCodeVerifier = codeVerifier;
const pkceChallenge = Token.generatePkceChallenge(pkceMethod, codeVerifier);
url += '&code_challenge=' + pkceChallenge;
url += '&code_challenge_method=' + pkceMethod;
this.callbackStorage.add(callback);
return url;
}
loadUserInfo() {
const url = this.getRealmUrl() + '/protocol/openid-connect/userinfo';
const headers = new HttpHeaders({ 'Accept': 'application/json', 'Authorization': 'bearer ' + this.accessToken });
return this.http.get(url, { headers: headers });
}
hasRealmRole(role) {
const access = this.realmAccess;
return !!access && access.roles.indexOf(role) >= 0;
}
hasResourceRole(role, resource) {
if (!this.resourceAccess) {
return false;
}
const access = this.resourceAccess[resource || this.keycloakConfig.clientId];
return !!access && access.roles.indexOf(role) >= 0;
}
isTokenExpired(minValidity) {
if (!this.tokenParsed || (!this.refreshToken && this.initOptions.flow !== KeycloakFlow.IMPLICIT)) {
throw new Error('Not authenticated');
}
let expiresIn = this.tokenParsed['exp'] - (new Date().getTime() / 1000) + this.timeSkew;
if (minValidity) {
expiresIn -= minValidity;
}
return expiresIn < 0;
}
isRefreshTokenExpired(minValidity) {
if (!this.tokenParsed || (!this.refreshToken && this.initOptions.flow !== KeycloakFlow.IMPLICIT)) {
throw new Error('Not authenticated');
}
let expiresIn = this.refreshTokenParsed['exp'] - (new Date().getTime() / 1000) + this.timeSkew;
if (minValidity) {
expiresIn -= minValidity;
}
return expiresIn < 0;
}
/**
* This method enables client applications to better integrate with resource servers protected by a Keycloak
* policy enforcer.
*
* In this case, the res