UNPKG

@ebondu/angular2-keycloak

Version:
1 lines 134 kB
{"version":3,"file":"ebondu-angular2-keycloak.mjs","sources":["../../../../projects/ebondu/angular-keycloak/src/lib/angular-keycloak.service.ts","../../../../projects/ebondu/angular-keycloak/src/lib/angular-keycloak.module.ts","../../../../projects/ebondu/angular-keycloak/src/lib/model/keycloak-config.model.ts","../../../../projects/ebondu/angular-keycloak/src/lib/adapter/keycloak.adapter.default.ts","../../../../projects/ebondu/angular-keycloak/src/lib/storage/keycloak.storage.local.ts","../../../../projects/ebondu/angular-keycloak/src/lib/util/keycloak.utils.URIParser.ts","../../../../projects/ebondu/angular-keycloak/src/lib/util/keycloak.utils.token.ts","../../../../projects/ebondu/angular-keycloak/src/lib/adapter/keycloak.adapter.cordova.ts","../../../../projects/ebondu/angular-keycloak/src/lib/storage/keycloak.storage.cookie.ts","../../../../projects/ebondu/angular-keycloak/src/lib/util/keycloak.utils.silent-check-login-iframe.ts","../../../../projects/ebondu/angular-keycloak/src/lib/util/keycloak.utils.check-3pCookies-iframe.ts","../../../../projects/ebondu/angular-keycloak/src/lib/service/keycloak.service.ts","../../../../projects/ebondu/angular-keycloak/src/lib/interceptor/keycloak.interceptor.ts","../../../../projects/ebondu/angular-keycloak/src/lib/util/keycloak.utils.check-login-iframe.ts","../../../../projects/ebondu/angular-keycloak/src/public_api.ts","../../../../projects/ebondu/angular-keycloak/src/ebondu-angular2-keycloak.ts"],"sourcesContent":["/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Injectable } from '@angular/core';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class AngularKeycloakService {\n\n constructor() {\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { NgModule } from '@angular/core';\n\n@NgModule({\n imports: []\n})\nexport class AngularKeycloakModule {\n}\n","/*\n * Copyright 2022 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InjectionToken } from '@angular/core';\n\nexport const KEYCLOAK_JSON_PATH = new InjectionToken<string>('keycloakJsonPath');\nexport const KEYCLOAK_INIT_OPTIONS = new InjectionToken<KeycloakInitOptions>('keycloakOptions');\nexport const KEYCLOAK_CONF = new InjectionToken<KeycloakConfiguration>('keycloakConfiguration');\n\nexport enum KeycloakAdapterName {CORDOVA = 'cordova', DEFAULT = 'default', ANY = 'any'}\n\nexport enum KeycloakOnLoad {LOGIN_REQUIRED = 'login-required', CHECK_SSO = 'check-sso'}\n\nexport enum KeycloakResponseMode {QUERY = 'query', FRAGMENT = 'fragment'}\n\nexport enum KeycloakResponseType {CODE = 'code', ID_TOKEN = 'id_token token', CODE_ID_TOKEN = 'code id_token token'}\n\nexport enum KeycloakFlow {STANDARD = 'standard', IMPLICIT = 'implicit', HYBRID = 'hybrid'}\n\nexport enum LogoutMethod {POST = 'post', GET = 'get'}\n\n\nexport interface KeycloakInitOptions {\n\n useNonce?: boolean;\n\n /**\n * Allows to use different adapter:\n *\n * - {string} default - using browser api for redirects\n * - {string} cordova - using cordova plugins\n * - {function} - allows to provide custom function as adapter.\n */\n adapter?: KeycloakAdapterName;\n\n /**\n * Specifies an action to do on load.\n */\n onLoad?: KeycloakOnLoad;\n\n /**\n * Set an initial value for the token.\n */\n token?: string;\n\n /**\n * Set an initial value for the refresh token.\n */\n refreshToken?: string;\n\n /**\n * Set an initial value for the id token (only together with `token` or\n * `refreshToken`).\n */\n idToken?: string;\n\n /**\n * Set an initial value for skew between local time and Keycloak server in\n * seconds (only together with `token` or `refreshToken`).\n */\n timeSkew?: number;\n\n /**\n * Set to enable/disable monitoring login state.\n * @default true\n */\n checkLoginIframe?: boolean;\n\n /**\n * Set the interval to check login state (in seconds).\n * @default 5\n */\n checkLoginIframeInterval?: number;\n\n /**\n * Set the redirect uri to silent check login state.\n */\n silentCheckSsoRedirectUri?: string;\n\n silentCheckSsoFallback?: boolean;\n\n /**\n * Set the OpenID Connect response mode to send to Keycloak upon login.\n * @default fragment After successful authentication Keycloak will redirect\n * to JavaScript application with OpenID Connect parameters\n * added in URL fragment. This is generally safer and\n * recommended over query.\n */\n responseMode?: KeycloakResponseMode;\n\n /**\n * Set the OpenID Connect flow.\n * @default standard\n */\n flow?: KeycloakFlow;\n\n pkceMethod?: string;\n\n logoutMethod?: LogoutMethod;\n\n scope?: string;\n}\n\nexport interface KeycloakLoginOptions {\n /**\n * Undocumented.\n */\n scope?: string;\n\n /**\n * Specifies the uri to redirect to after login.\n */\n redirectUri?: string;\n\n /**\n * By default the login screen is displayed if the user is not logged into\n * Keycloak. To only authenticate to the application if the user is already\n * logged in and not display the login page if the user is not logged in, set\n * this option to `'none'`. To always require re-authentication and ignore\n * SSO, set this option to `'login'`.\n */\n prompt?: 'none' | 'login';\n\n /**\n * If value is `'register'` then user is redirected to registration page,\n * otherwise to login page.\n */\n action?: 'register';\n\n /**\n * Used just if user is already authenticated. Specifies maximum time since\n * the authentication of user happened. If user is already authenticated for\n * longer time than `'maxAge'`, the SSO is ignored and he will need to\n * authenticate again.\n */\n maxAge?: number;\n\n /**\n * Used to pre-fill the username/email field on the login form.\n */\n loginHint?: string;\n\n /**\n * Used to tell Keycloak which IDP the user wants to authenticate with.\n */\n idpHint?: string;\n\n /**\n * Sets the 'ui_locales' query param in compliance with section 3.1.2.1\n * of the OIDC 1.0 specification.\n */\n locale?: string;\n\n /**\n * Specifies the desired Keycloak locale for the UI. This differs from\n * the locale param in that it tells the Keycloak server to set a cookie and update\n * the user's profile to a new preferred locale.\n */\n kcLocale?: string;\n}\n\nexport interface KeycloakConfiguration {\n realm: string;\n authServerUrl: string;\n clientId: string;\n clientSecret?: string;\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport { KeycloakService } from '../service/keycloak.service';\n\n/**\n * Default adapter for web browsers\n */\nexport class DefaultAdapter {\n\n constructor(private keycloak: KeycloakService) {\n }\n\n public login(options: any) {\n window.location.href = this.keycloak.createLoginUrl(options);\n }\n\n public logout(options: any) {\n window.location.href = this.keycloak.createLogoutUrl(options);\n }\n\n public register(options: any) {\n window.location.href = this.keycloak.createRegisterUrl(options);\n }\n\n public accountManagement() {\n window.location.href = this.keycloak.createAccountUrl({});\n }\n\n public passwordManagement() {\n window.location.href = this.keycloak.createChangePasswordUrl({});\n }\n\n public redirectUri(options: any, encodeHash: boolean): string {\n\n if (arguments.length === 1) {\n encodeHash = true;\n }\n\n if (options && options.redirectUri) {\n return options.redirectUri;\n } else {\n let redirectUri = location.href;\n if (location.hash && encodeHash) {\n redirectUri = redirectUri.substring(0, location.href.indexOf('#'));\n redirectUri += (redirectUri.indexOf('?') === -1 ? '?' : '&') + 'redirect_fragment=' +\n encodeURIComponent(location.hash.substring(1));\n }\n return redirectUri;\n }\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * To store Keycloak objects like tokens using a localStorage.\n */\nexport class LocalStorage {\n\n public clearExpired() {\n const time = new Date().getTime();\n for (let i = 1; i <= localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key && key.indexOf('kc-callback-') === 0) {\n const value = localStorage.getItem(key);\n if (value) {\n try {\n const expires = JSON.parse(value).expires;\n if (!expires || expires < time) {\n localStorage.removeItem(key);\n }\n } catch (err) {\n localStorage.removeItem(key);\n }\n }\n }\n }\n }\n\n public get(state: string) {\n if (!state) {\n return;\n }\n\n const key = 'kc-callback-' + state;\n let value = localStorage.getItem(key);\n if (value) {\n localStorage.removeItem(key);\n value = JSON.parse(value);\n }\n\n this.clearExpired();\n return value;\n }\n\n public add(state: any) {\n this.clearExpired();\n\n const key = 'kc-callback-' + state.state;\n state.expires = new Date().getTime() + (60 * 60 * 1000);\n localStorage.setItem(key, JSON.stringify(state));\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * URI parser.\n */\nexport class URIParser {\n\n static initialParse(uriToParse: string, responseMode: string) {\n let baseUri: string;\n let queryString: string;\n let fragmentString: string;\n\n const questionMarkIndex = uriToParse.indexOf('?');\n let fragmentIndex = uriToParse.indexOf('#', questionMarkIndex + 1);\n if (questionMarkIndex === -1 && fragmentIndex === -1) {\n baseUri = uriToParse;\n } else if (questionMarkIndex !== -1) {\n baseUri = uriToParse.substring(0, questionMarkIndex);\n queryString = uriToParse.substring(questionMarkIndex + 1);\n if (fragmentIndex !== -1) {\n fragmentIndex = queryString.indexOf('#');\n fragmentString = queryString.substring(fragmentIndex + 1);\n queryString = queryString.substring(0, fragmentIndex);\n }\n } else {\n baseUri = uriToParse.substring(0, fragmentIndex);\n fragmentString = uriToParse.substring(fragmentIndex + 1);\n }\n\n return {baseUri: baseUri, queryString: queryString, fragmentString: fragmentString};\n }\n\n static parseParams(paramString: string) {\n const result: any = {};\n const params = paramString.split('&');\n for (let i = 0; i < params.length; i++) {\n const p = params[i].split('=');\n const paramName = decodeURIComponent(p[0]);\n const paramValue = decodeURIComponent(p[1]);\n result[paramName] = paramValue;\n }\n return result;\n }\n\n static handleQueryParam(paramName: string, paramValue: string, oauth: any): boolean {\n const supportedOAuthParams = ['code', 'state', 'error', 'session_state', 'error_description'];\n\n for (let i = 0; i < supportedOAuthParams.length; i++) {\n if (paramName === supportedOAuthParams[i]) {\n oauth[paramName] = paramValue;\n return true;\n }\n }\n return false;\n }\n\n\n static parseUri(uriToParse: string, responseMode: string) {\n const parsedUri = this.initialParse(decodeURIComponent(uriToParse), responseMode);\n\n let queryParams: any = {};\n if (parsedUri.queryString) {\n queryParams = this.parseParams(parsedUri.queryString);\n }\n\n const oauth: any = {newUrl: parsedUri.baseUri};\n\n Object.keys(queryParams).forEach(param => {\n switch (param) {\n case 'redirect_fragment':\n oauth.fragment = queryParams[param];\n break;\n case 'prompt':\n oauth.prompt = queryParams[param];\n break;\n default:\n if (responseMode !== 'query' || !this.handleQueryParam(param, queryParams[param], oauth)) {\n oauth.newUrl += (oauth.newUrl.indexOf('?') === -1 ? '?' : '&') + param + '=' + queryParams[param];\n }\n break;\n }\n });\n\n if (responseMode === 'fragment') {\n let fragmentParams: any = {};\n if (parsedUri.fragmentString) {\n fragmentParams = this.parseParams(parsedUri.fragmentString);\n }\n Object.keys(fragmentParams).forEach(param => {\n oauth[param] = fragmentParams[param];\n });\n }\n return oauth;\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Token utility\n */\n\nimport { fromByteArray } from 'base64-js';\nimport { sha256 } from 'js-sha256';\n\nexport class Token {\n\n static decodeToken(str: string): string {\n str = str.split('.')[1];\n\n str = str.replace('/-/g', '+');\n str = str.replace('/_/g', '/');\n switch (str.length % 4) {\n case 0:\n break;\n case 2:\n str += '==';\n break;\n case 3:\n str += '=';\n break;\n default:\n throw new Error('Invalid token');\n }\n\n str = (str + '===').slice(0, str.length + (str.length % 4));\n str = str.replace(/-/g, '+').replace(/_/g, '/');\n\n str = decodeURIComponent(escape(atob(str)));\n\n str = JSON.parse(str);\n return str;\n }\n\n static generateRandomData(len) {\n // use web crypto APIs if possible\n let array = null;\n const crypto = window.crypto;\n if (crypto && crypto.getRandomValues && window.Uint8Array) {\n array = new Uint8Array(len);\n crypto.getRandomValues(array);\n return array;\n }\n\n // fallback to Math random\n array = new Array(len);\n for (let j = 0; j < array.length; j++) {\n array[j] = Math.floor(256 * Math.random());\n }\n return array;\n }\n\n static generateCodeVerifier(len) {\n return Token.generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');\n }\n\n static generateRandomString(len, alphabet) {\n const randomData = this.generateRandomData(len);\n const chars = new Array(len);\n for (let i = 0; i < len; i++) {\n chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);\n }\n return String.fromCharCode.apply(null, chars);\n }\n\n static generatePkceChallenge(pkceMethod, codeVerifier) {\n switch (pkceMethod) {\n // The use of the \"plain\" method is considered insecure and therefore not supported.\n case 'S256':\n // hash codeVerifier, then encode as url-safe base64 without padding\n const hashBytes = new Uint8Array(sha256.arrayBuffer(codeVerifier));\n const encodedHash = fromByteArray(hashBytes)\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/\\=/g, '');\n return encodedHash;\n default:\n throw new Error('Invalid value for pkceMethod');\n }\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { KeycloakService } from '../service/keycloak.service';\n\ndeclare var window: any;\n\n/**\n * Cordova adapter for hybrid apps.\n */\nexport class CordovaAdapter {\n\n constructor(private keycloak: KeycloakService) {\n\n }\n\n public login(options: any) {\n // let promise = Keycloak.createPromise();\n let o = 'location=no';\n if (options && options.prompt === 'none') {\n o += ',hidden=yes';\n }\n const loginUrl = this.keycloak.createLoginUrl(options);\n\n // console.log('opening login frame from cordova: ' + loginUrl);\n if (!window.cordova) {\n throw new Error('Cannot authenticate via a web browser');\n }\n\n if (!window.cordova.InAppBrowser || !window.cordova.plugins.browsertab) {\n throw new Error('The Apache Cordova InAppBrowser/BrowserTab plugins was not found and are required');\n }\n\n let ref: any;\n // let ref = window.cordova.InAppBrowser.open(loginUrl, '_blank', o);\n // let ref = window.cordova.InAppBrowser.open(loginUrl, '_system', o);\n let completed = false;\n\n window.cordova.plugins.browsertab.themeable.isAvailable(\n function (result: any) {\n if (!result) {\n ref = window.cordova.InAppBrowser.open(loginUrl, '_system');\n ref.addEventListener('loadstart', function (event: any) {\n if (event.url.indexOf('http://localhost') === 0) {\n const callback = this.keycloak.parseCallback(event.url);\n this.keycloak.processCallback(callback).subscribe(processed => {\n ref.close();\n completed = true;\n });\n }\n });\n\n ref.addEventListener('loaderror', function (event: any) {\n if (!completed) {\n if (event.url.indexOf('http://localhost') === 0) {\n\n const callback = this.keycloak.parseCallback(event.url);\n this.keycloak.processCallback(callback).subscribe(processed => {\n this.closeBrowserTab();\n // ref.close();\n // completed = true;\n });\n } else {\n this.closeBrowserTab();\n // ref.close();\n }\n }\n });\n } else {\n this.openBrowserTab(loginUrl, options);\n }\n },\n function (isAvailableError: any) {\n console.error('failed to query availability of in-app browser tab');\n }\n );\n }\n\n public closeBrowserTab() {\n const cordova = window.cordova;\n cordova.plugins.browsertab.themeable.close();\n // completed = true;\n }\n\n public logout(options: any) {\n const cordova = window.cordova;\n const logoutUrl = this.keycloak.createLogoutUrl(options);\n let ref: any;\n let error: any;\n\n cordova.plugins.browsertab.themeable.isAvailable(\n function (result: any) {\n if (!result) {\n ref = cordova.InAppBrowser.open(logoutUrl, '_system');\n ref.addEventListener('loadstart', function (event: any) {\n if (event.url.indexOf('http://localhost') === 0) {\n this.ref.close();\n this.closeBrowserTab();\n }\n });\n\n ref.addEventListener('loaderror', function (event: any) {\n if (event.url.indexOf('http://localhost') === 0) {\n this.ref.close();\n this.closeBrowserTab();\n } else {\n error = true;\n this.ref.close();\n this.closeBrowserTab();\n }\n });\n\n ref.addEventListener('exit', function (event: any) {\n if (error) {\n console.error('listener of in-app browser tab exited due to error', error);\n } else {\n this.keycloak.clearToken({});\n }\n });\n } else {\n this.openBrowserTab(logoutUrl, options);\n }\n },\n function (isAvailableError: any) {\n console.error('failed to query availability of in-app browser tab', isAvailableError);\n }\n );\n }\n\n public register(options: any) {\n const registerUrl = this.keycloak.createRegisterUrl({});\n window.cordova.plugins.browsertab.themeable.isAvailable(\n function (result: any) {\n if (!result) {\n window.cordova.InAppBrowser.open(registerUrl, '_system');\n } else {\n this.openBrowserTab(registerUrl, options);\n }\n },\n function (isAvailableError: any) {\n console.error('failed to query availability of in-app browser tab', isAvailableError);\n }\n );\n }\n\n public accountManagement(options: any) {\n const accountUrl = this.keycloak.createAccountUrl({});\n window.cordova.plugins.browsertab.themeable.isAvailable(\n function (result: any) {\n if (!result) {\n window.cordova.InAppBrowser.open(accountUrl, '_system');\n } else {\n this.openBrowserTab(accountUrl, options);\n }\n },\n function (isAvailableError: any) {\n console.error('failed to query availability of in-app browser tab', isAvailableError);\n }\n );\n }\n\n\n public passwordManagement(options: any) {\n const accountUrl = this.keycloak.createChangePasswordUrl({});\n window.cordova.plugins.browsertab.themeable.isAvailable(\n function (result: any) {\n if (!result) {\n window.cordova.InAppBrowser.open(accountUrl, '_system');\n } else {\n this.openBrowserTab(accountUrl, options);\n }\n },\n function (isAvailableError: any) {\n console.error('failed to query availability of in-app browser tab', isAvailableError);\n }\n );\n }\n\n public redirectUri(options: any): any {\n if (options.redirectUri) {\n return options.redirectUri;\n } else {\n return 'http://localhost';\n }\n }\n\n private openBrowserTab(url: String, options: any) {\n const cordova = window.cordova;\n if (options.toolbarColor) {\n cordova.plugins.browsertab.themeable.openUrl(url, options);\n } else {\n cordova.plugins.browsertab.themeable.openUrl(url);\n }\n }\n}\n","/*\n * Copyright 2018 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * To store Keycloak objects like tokens using a cookie.\n */\nexport class CookieStorage {\n\n public getCookie = function (key: any) {\n const name = key + '=';\n const ca = document.cookie.split(';');\n for (let i = 0; i < ca.length; i++) {\n let c = ca[i];\n while (c.charAt(0) === ' ') {\n c = c.substring(1);\n }\n if (c.indexOf(name) === 0) {\n return c.substring(name.length, c.length);\n }\n }\n return '';\n };\n\n public get(state: string) {\n if (!state) {\n return;\n }\n\n const value = this.getCookie('kc-callback-' + state);\n this.setCookie('kc-callback-' + state, '', this.cookieExpiration(-100));\n if (value) {\n return JSON.parse(value);\n }\n }\n\n public add(state: any) {\n this.setCookie('kc-callback-' + state.state, JSON.stringify(state), this.cookieExpiration(60));\n }\n\n public removeItem(key: any) {\n this.setCookie(key, '', this.cookieExpiration(-100));\n }\n\n public cookieExpiration(minutes: number) {\n const exp = new Date();\n exp.setTime(exp.getTime() + (minutes * 60 * 1000));\n return exp;\n }\n\n public setCookie(key: string, value: string, expirationDate: Date) {\n const cookie = key + '=' + value + '; '\n + 'expires=' + expirationDate.toUTCString() + '; ';\n document.cookie = cookie;\n }\n}\n","/*\n * Copyright 2022 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { KeycloakService } from '../service/keycloak.service';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n/**\n * Silent login check Iframe utility\n */\nexport class KeycloakSilentCheckLoginIframe {\n private iframe: any;\n private iframeSrc: string;\n\n constructor(private keycloak: KeycloakService, silentRedirectUri: string) {\n this.iframeSrc = this.keycloak.createLoginUrl({\n prompt: 'none',\n redirectUri: silentRedirectUri\n });\n this.initIframe();\n }\n\n initIframe() {\n this.iframe = document.createElement('iframe');\n this.iframe.setAttribute('src', this.iframeSrc);\n this.iframe.style.display = 'none';\n this.iframe.setAttribute('title', 'keycloak-silent-check-sso');\n document.body.appendChild(this.iframe);\n window.addEventListener('message', () => this.processSilentLoginCallbackMessage(event), false);\n }\n\n private processSilentLoginCallbackMessage(event: any) {\n const origin = this.iframeSrc.substring(0, this.iframeSrc.indexOf('/', 8));\n // console.log('checking iframe message callback..' + event.data + ' ' + event.origin);\n if ((event.origin !== window.location.origin) || (this.iframe.contentWindow !== event.source)) {\n // console.log('event is not coming from the iframe, ignoring it');\n return;\n }\n const oauth = this.keycloak.parseCallback(event.data);\n if (!!oauth) {\n this.keycloak.processCallback(oauth).subscribe(() => console.log('Silent login ended'));\n }\n document.body.removeChild(this.iframe);\n window.removeEventListener('message', () => this.processSilentLoginCallbackMessage(event), false);\n }\n}\n","/*\n * Copyright 2022 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { KeycloakService } from '../service/keycloak.service';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n/**\n * 3Party cookie Iframe utility\n */\nexport class KeycloakCheck3pCookiesIframe {\n private iframe: any;\n private interval: number;\n private iframeSrc: string;\n private supportedBS: BehaviorSubject<boolean>;\n public supportedObs: Observable<boolean>;\n\n constructor(private keycloak: KeycloakService) {\n this.iframeSrc = this.keycloak.getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';\n this.supportedBS = new BehaviorSubject<any>(null);\n this.supportedObs = this.supportedBS.asObservable();\n this.initIframe();\n }\n\n initIframe() {\n this.iframe = document.createElement('iframe');\n this.iframe.setAttribute('src', this.iframeSrc);\n this.iframe.setAttribute('title', 'keycloak-3p-check-iframe' );\n this.iframe.style.display = 'none';\n document.body.appendChild(this.iframe);\n window.addEventListener('message', () => this.process3pCookieCallbackMessage(event), false);\n }\n\n private process3pCookieCallbackMessage(event: any) {\n // console.log('checking iframe message callback..' + event.data + ' ' + event.origin);\n if (this.iframe.contentWindow !== event.source) {\n // console.log('event is not coming from the iframe, ignoring it');\n return;\n }\n // console.log('Checking iframe message ' + event.data);\n if (event.data !== 'supported' && event.data !== 'unsupported') {\n return;\n }\n if (event.data === 'unsupported') {\n console.warn('[KEYCLOAK] 3rd party cookies aren\\'t supported by this browser.' +\n ' checkLoginIframe and silent check-sso are not available.'\n );\n this.supportedBS.next(false);\n } else {\n this.supportedBS.next(true);\n }\n document.body.removeChild(this.iframe);\n window.removeEventListener('message', () => this.process3pCookieCallbackMessage(event));\n }\n}\n","/*\n * Copyright 2024 ebondu and/or its affiliates\n * and other contributors as indicated by the @author tags.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { inject, Injectable, Injector, NgZone, PLATFORM_ID } from '@angular/core';\nimport { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';\nimport { BehaviorSubject, EMPTY, Observable, of } from 'rxjs';\n\nimport { v4 as uuidv4 } from 'uuid';\nimport { DefaultAdapter } from '../adapter/keycloak.adapter.default';\nimport { LocalStorage } from '../storage/keycloak.storage.local';\nimport { URIParser } from '../util/keycloak.utils.URIParser';\nimport { Token } from '../util/keycloak.utils.token';\nimport {\n KEYCLOAK_CONF,\n KEYCLOAK_INIT_OPTIONS,\n KEYCLOAK_JSON_PATH,\n KeycloakAdapterName,\n KeycloakFlow,\n KeycloakOnLoad,\n KeycloakResponseMode,\n KeycloakResponseType\n} from '../model/keycloak-config.model';\nimport { filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';\nimport { CordovaAdapter } from '../adapter/keycloak.adapter.cordova';\nimport { CookieStorage } from '../storage/keycloak.storage.cookie';\nimport { KeycloakCheckLoginIframe } from '../util/keycloak.utils.check-login-iframe';\nimport { KeycloakSilentCheckLoginIframe } from '../util/keycloak.utils.silent-check-login-iframe';\nimport { KeycloakCheck3pCookiesIframe } from '../util/keycloak.utils.check-3pCookies-iframe';\nimport { isPlatformBrowser } from '@angular/common';\n\n/**\n * Keycloak core classes to manage tokens with a keycloak server.\n *\n * Used for login, logout, register, account management, profile.\n * Provide Angular Observable objects for initialization, authentication, token expiration.\n *\n */\n\n@Injectable({\n providedIn: 'root'\n})\nexport class KeycloakService {\n\n public initializedObs: Observable<boolean>;\n public initializedAuthzObs: Observable<boolean>;\n public authenticationObs: Observable<boolean>;\n public tokenExpiredObs: Observable<boolean>;\n public authenticationErrorObs: Observable<any>;\n // tokens\n public accessToken: string;\n public tokenParsed: any;\n public sessionId: any;\n // Observables\n private initBS: BehaviorSubject<boolean>;\n private initAuthzBS: BehaviorSubject<boolean>;\n private authenticationsBS: BehaviorSubject<boolean>;\n private tokenExpiredBS: BehaviorSubject<boolean>;\n private authenticationErrorBS: BehaviorSubject<any>;\n private refreshToken: string;\n private refreshTokenParsed: any;\n private rpt: string;\n private idToken: string;\n private idTokenParsed: any;\n // keycloak conf\n private umaConfig: any;\n private adapter;\n private callbackStorage;\n private responseType;\n private timeSkew;\n private tokenTimeoutHandle;\n private subject: any;\n private realmAccess;\n private resourceAccess;\n private loginIframe: KeycloakCheckLoginIframe;\n\n readonly #injector = inject(Injector);\n readonly #platformId = inject(PLATFORM_ID);\n readonly #ngZone = inject(NgZone);\n readonly #configUrl = inject(KEYCLOAK_JSON_PATH, {optional: true});\n public keycloakConfig = inject(KEYCLOAK_CONF, {optional: true});\n public readonly initOptions = inject(KEYCLOAK_INIT_OPTIONS);\n\n get http(): HttpClient {\n return this.#injector.get(HttpClient);\n }\n\n constructor() {\n\n this.initBS = new BehaviorSubject(false);\n this.initializedObs = this.initBS.asObservable();\n\n this.initAuthzBS = new BehaviorSubject(false);\n this.initializedAuthzObs = this.initAuthzBS.asObservable();\n\n this.authenticationsBS = new BehaviorSubject(false);\n this.authenticationObs = this.authenticationsBS.asObservable();\n\n this.tokenExpiredBS = new BehaviorSubject<boolean>(false);\n this.tokenExpiredObs = this.tokenExpiredBS.asObservable();\n\n this.authenticationErrorBS = new BehaviorSubject<any>(null);\n this.authenticationErrorObs = this.authenticationErrorBS.asObservable();\n // console.log('Keycloak service created with init options and configuration file', initOptions, configUrl);\n\n if (!isPlatformBrowser(this.#platformId)) {\n // console.log('Keycloak service init only available on browser platform');\n this.initBS.next(false);\n } else {\n if (!globalThis.isSecureContext) {\n 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');\n }\n if (this.#configUrl) {\n this.http.get(this.#configUrl).subscribe({\n next: (config) => {\n this.keycloakConfig = {\n authServerUrl: config['auth-server-url'],\n realm: config['realm'],\n clientId: config['resource'],\n clientSecret: (config['credentials'] || {})['secret']\n };\n // console.log('Conf loaded', this.keycloakConfig);\n this.initService();\n },\n error: () => {\n // console.log('Unable to load keycloak.json', error);\n this.initBS.next(false);\n }\n });\n } else if (this.keycloakConfig) {\n this.initService();\n } else {\n // console.log('Keycloak service init fails : no keycloak.json or configuration provided');\n this.initBS.next(false);\n }\n\n this.initializedObs.pipe(filter(initialized => !!initialized)).subscribe(next => {\n // console.log('Keycloak initialized, initializing authz service', this);\n if (next) {\n const url = this.keycloakConfig.authServerUrl + '/realms/' + this.keycloakConfig.realm + '/.well-known/uma2-configuration';\n this.http.get(url).subscribe({\n next: (authz) => {\n // console.log('Authz configuration file loaded, continuing authz');\n this.umaConfig = authz;\n this.initAuthzBS.next(true);\n },\n error: () => {\n // console.log('unable to get uma file', error);\n this.initAuthzBS.next(false);\n }\n });\n }\n });\n }\n }\n\n public parseCallback(url: string): any {\n const oauth: any = URIParser.parseUri(url, this.initOptions.responseMode);\n const state: string = oauth.state;\n const oauthState = this.callbackStorage.get(state);\n\n if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {\n oauth.valid = true;\n oauth.redirectUri = oauthState.redirectUri;\n oauth.storedNonce = oauthState.nonce;\n oauth.prompt = oauthState.prompt;\n oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;\n if (oauth.fragment) {\n oauth.newUrl += '#' + oauth.fragment;\n }\n return oauth;\n }\n }\n\n public processCallback(oauth: any): Observable<boolean> {\n return new Observable<boolean>((observer: any) => {\n const code = oauth.code;\n const error = oauth.error;\n const prompt = oauth.prompt;\n const timeLocal = new Date().getTime();\n\n if (error) {\n const errorData = {error: error, error_description: oauth.error_description};\n this.authenticationErrorBS.next(errorData);\n if (prompt !== 'none') {\n // console.log('error while processing callback');\n observer.next(false);\n }\n return;\n } else if ((this.initOptions.flow !== KeycloakFlow.STANDARD) && (oauth.access_token || oauth.id_token)) {\n this.authSuccess(oauth.access_token, null, oauth.id_token, true, timeLocal, oauth);\n observer.next(true);\n }\n\n if ((this.initOptions.flow !== KeycloakFlow.IMPLICIT) && code) {\n\n let withCredentials = false;\n const url = this.getRealmUrl() + '/protocol/openid-connect/token';\n let params: HttpParams = new HttpParams();\n params = params.set('code', code);\n params = params.set('grant_type', 'authorization_code');\n\n let headers = new HttpHeaders();\n headers = headers.set('Content-type', 'application/x-www-form-urlencoded');\n\n if (this.keycloakConfig.clientId && this.keycloakConfig.clientSecret) {\n headers = headers.set('Authorization', 'Basic ' + btoa(this.keycloakConfig.clientId + ':' + this.keycloakConfig.clientSecret));\n withCredentials = true;\n } else {\n params = params.set('client_id', this.keycloakConfig.clientId);\n }\n params = params.set('redirect_uri', oauth.redirectUri);\n\n if (oauth.pkceCodeVerifier) {\n params = params.set('code_verifier', oauth.pkceCodeVerifier);\n }\n\n const options = {headers: headers, withCredentials: withCredentials};\n this.http.post(url, params, options).subscribe({\n next: (token) => {\n this.authSuccess(\n token['access_token'],\n token['refresh_token'],\n token['id_token'],\n this.initOptions.flow === KeycloakFlow.STANDARD,\n timeLocal,\n oauth);\n this.authenticationsBS.next(true);\n observer.next(true);\n },\n error: (errorToken) => {\n this.authenticationErrorBS.next({error: errorToken, error_description: 'unable to get token from server'});\n // console.log('Unable to get token', errorToken);\n observer.next(false);\n }\n });\n }\n });\n }\n\n login(options: any) {\n return this.adapter.login(options);\n }\n\n\n // ###################################\n // ####### Keycloak methods ######\n // ###################################\n\n logout(options: any) {\n return this.adapter.logout(options);\n }\n\n updateToken(minValidity: number): Observable<string> {\n\n minValidity = minValidity || 5;\n\n if (!this.isTokenExpired(minValidity)) {\n // console.log('token still valid');\n return of(this.accessToken);\n } else {\n if (this.isRefreshTokenExpired(5)) {\n this.login(this.keycloakConfig);\n return EMPTY;\n } else {\n // console.log('refreshing token');\n let params: HttpParams = new HttpParams();\n params = params.set('grant_type', 'refresh_token');\n params = params.set('refresh_token', this.refreshToken);\n\n const url = this.getRealmUrl() + '/protocol/openid-connect/token';\n let headers = new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded'});\n\n let withCredentials = false;\n if (this.keycloakConfig.clientId && this.keycloakConfig.clientSecret) {\n headers = headers.append(\n 'Authorization',\n 'Basic ' + btoa(this.keycloakConfig.clientId + ': ' + this.keycloakConfig.clientSecret));\n withCredentials = true;\n } else {\n params = params.set('client_id', this.keycloakConfig.clientId);\n }\n\n let timeLocal = new Date().getTime();\n return this.http.post(url, params, {headers: headers, withCredentials: withCredentials}).pipe(\n map((token: any) => {\n timeLocal = (timeLocal + new Date().getTime()) / 2;\n this.setToken(token['access_token'], token['refresh_token'], token['id_token'], true);\n this.timeSkew = Math.floor(timeLocal / 1000) - this.tokenParsed.iat;\n return token['access_token'];\n })\n );\n }\n }\n }\n\n register(options: any) {\n return this.adapter.register(options);\n }\n\n accountManagement(options: any) {\n return this.adapter.accountManagement(options);\n }\n\n loadChangePassword(options: any) {\n return this.adapter.passwordManagement(options);\n }\n\n loadUserProfile(): Observable<any> {\n // need to refresh token to get account-console as aud\n let paramsToSend: HttpParams = new HttpParams();\n let headersToSend = new HttpHeaders();\n headersToSend = headersToSend.set('Content-type', 'application/x-www-form-urlencoded');\n paramsToSend = paramsToSend.set('client_id', this.keycloakConfig.clientId);\n paramsToSend = paramsToSend.set('grant_type', 'refresh_token');\n paramsToSend = paramsToSend.set('refresh_token', this.refreshToken);\n headersToSend = headersToSend.set('Authorization', 'bearer ' + this.accessToken);\n const url = this.getRealmUrl() + '/account/';\n return this.http.post(this.umaConfig?.token_endpoint, paramsToSend, {withCredentials: false, headers: headersToSend})\n .pipe(mergeMap((token: any) => {\n const headers = new HttpHeaders({'Authorization': 'bearer ' + token.access_token});\n return this.http.get(url, {headers: headers, withCredentials: false});\n }));\n }\n\n updateUserProfile(profile: any): Observable<any> {\n // need to refresh token to get account-console as aud\n let paramsToSend: HttpParams = new HttpParams();\n let headersToSend = new HttpHeaders();\n headersToSend = headersToSend.set('Content-type', 'application/x-www-form-urlencoded');\n paramsToSend = paramsToSend.set('client_id', this.keycloakConfig.clientId);\n paramsToSend = paramsToSend.set('grant_type', 'refresh_token');\n paramsToSend = paramsToSend.set('refresh_token', this.refreshToken);\n headersToSend = headersToSend.set('Authorization', 'bearer ' + this.accessToken);\n const url = this.getRealmUrl() + '/account/';\n return this.http.post(this.umaConfig?.token_endpoint, paramsToSend, {withCredentials: true, headers: headersToSend})\n .pipe(mergeMap((token: any) => {\n const headers = new HttpHeaders({'Authorization': 'bearer ' + token.access_token});\n return this.http.post(url, profile, {headers: headers, withCredentials: false});\n }));\n }\n\n createDeleteAccountUrl(options?: any): string {\n const state = uuidv4();\n const nonce = this.initOptions.useNonce ? uuidv4() : null;\n // const redirectUri = this.getRealmUrl() + '/account/#/personal-info';\n const redirectUri = this.adapter.redirectUri({});\n const callback: {state, nonce, redirectUri, pkceCodeVerifier? } = {\n state: state,\n nonce: nonce,\n redirectUri: redirectUri\n };\n\n const action = 'auth';\n\n let url = this.getRealmUrl()\n + '/protocol/openid-connect/' + action\n + '?client_id=' + encodeURIComponent(this.keycloakConfig.clientId)\n + '&redirect_uri=' + encodeURIComponent(redirectUri)\n + '&state=' + encodeURIComponent(state)\n + '&response_mode=' + encodeURIComponent(this.initOptions.responseMode)\n + '&response_type=' + encodeURIComponent(this.responseType)\n + '&scope=' + encodeURIComponent('openid')\n + '&kc_action=' + encodeURIComponent('delete_account');\n\n if (options.useNonce) {\n url += '&nonce=' + encodeURIComponent(nonce);\n }\n\n let codeVerifier;\n const pkceMethod = this.initOptions.pkceMethod;\n codeVerifier = Token.generateCodeVerifier(96);\n callback.pkceCodeVerifier = codeVerifier;\n const pkceChallenge = Token.generatePkceChallenge(pkceMethod, codeVerifier);\n url += '&code_challenge=' + pkceChallenge;\n url += '&code_challenge_method=' + pkceMethod;\n\n this.callbackStorage.add(callback);\n return url;\n }\n\n createUpdateProfileUrl(options?: any): string {\n const state = uuidv4();\n const nonce = this.initOptions.useNonce ? uuidv4() : null;\n const redirectUri = this.adapter.redirectUri({});\n const callback: {state, nonce, redirectUri, pkceCodeVerifier? } = {\n state: state,\n nonce: nonce,\n redirectUri: redirectUri\n };\n\n const action = 'auth';\n\n let url = this.getRealmUrl()\n + '/protocol/openid-connect/' + action\n + '?client_id=' + encodeURIComponent(this.keycloakConfig.clientId)\n + '&redirect_uri=' + encodeURIComponent(redirectUri)\n + '&state=' + encodeURIComponent(state)\n + '&response_mode=' + encodeURIComponent(this.initOptions.responseMode)\n + '&response_type=' + encodeURIComponent(this.responseType)\n + '&scope=' + encodeURIComponent('openid')\n + '&kc_action=' + encodeURIComponent('UPDATE_PROFILE');\n\n if (options.useNonce) {\n url += '&nonce=' + encodeURIComponent(nonce);\n }\n\n l