Skip to content
Snippets Groups Projects
Select Git revision
  • 798971d636abf49a3a26db0bbfc99c9fcc9ae1bc
  • main default protected
  • renovate/lock-file-maintenance
  • demo protected
  • person-select-custom
  • dbp-translation-component
  • icon-set-mapping
  • port-i18next-parser
  • remove-sentry
  • favorites-and-recent-files
  • revert-6c632dc6
  • lit2
  • advertisement
  • wc-part
  • automagic
  • publish
  • wip-cleanup
  • demo-file-handling
18 results

keycloak.js

Blame
  • keycloak.js 7.95 KiB
    import {EventTarget} from "event-target-shim";  // Because EventTarget() doesn't exist on Safari
    
    
    /**
     * Imports the keycloak JS API as if it was a module.
     *
     * @param baseUrl {string}
     */
    async function importKeycloak(baseUrl) {
        const keycloakSrc = baseUrl + '/js/keycloak.min.js';
        // Importing will write it to window so we take it from there
        await import(keycloakSrc);
        if (importKeycloak._keycloakMod !== undefined)
            return importKeycloak._keycloakMod;
        importKeycloak._keycloakMod = window.Keycloak;
        delete window.Keycloak;
        return importKeycloak._keycloakMod;
    }
    
    
    const promiseTimeout = function(ms, promise) {
        let timeout = new Promise((resolve, reject) => {
          let id = setTimeout(() => {
            clearTimeout(id);
            reject('Timed out in '+ ms + 'ms.');
          }, ms);
        });
    
        return Promise.race([
          promise,
          timeout
        ]);
    };
    
    
    /**
     * Returns a URL for a relative path or URL
     *
     * @param {string} urlOrPath
     */
    const ensureURL = function(urlOrPath) {
        try {
            return new URL(urlOrPath).href;
        } catch (e) {
            return new URL(urlOrPath, window.location.href).href;
        }
    };
    
    
    /**
     * Wraps the keycloak API to support async/await, adds auto token refreshing and consolidates all
     * events into one native "changed" event
     * 
     * The "changed" event has the real keycloak instance as "detail"
     */
    export class KeycloakWrapper extends EventTarget {
    
        constructor(baseURL, realm, clientId, silentCheckSsoUri, idpHint) {
            super();
    
            this._baseURL = baseURL;
            this._realm = realm;
            this._clientId = clientId;
            this._keycloak = null;
            this._initDone = false;
            this._silentCheckSsoUri = silentCheckSsoUri;
            this._idpHint = idpHint;
            this._checkId = null;
    
            /* Minimum validity of the token in seconds */
            this.MIN_VALIDITY = 20;
    
            /* Interval at which the token validity is checked, in seconds */
            this.CHECK_INTERVAL = 10;
    
            /* Enables extra debug logging */
            this.DEBUG = false;
    
            this._onVisibilityChanged = this._onVisibilityChanged.bind(this);
            document.addEventListener("visibilitychange", this._onVisibilityChanged);
        }
    
        /**
         * This needs to be called or the instance will leak;
         */
        close() {
            document.removeEventListener("visibilitychange", this._onVisibilityChanged);
        }
    
        _onVisibilityChanged() {
            let isVisible = (document.visibilityState === 'visible');
            if (isVisible && this._keycloak.authenticated) {
                this._checkTokeHasExpired();
            }
        }
    
        _onChanged() {
            const event = new CustomEvent("changed", {
                detail: this._keycloak,
                bubbles: true,
                composed: true
            });
            this.dispatchEvent(event);
        }
    
        _onReady(authenticated) {
            // Avoid emitting changed when nothing has changed on init()
            if (authenticated)
                this._onChanged();
        }
    
        async _onTokenExpired() {
            console.log('Token has expired');
            let refreshed = false;
    
            try {
                // -1 means force a refresh
                refreshed = await this._keycloak.updateToken(-1);
            } catch (error) {
                console.log('Failed to refresh the token', error);
                return;
            }
    
            console.assert(refreshed, "token should have been refreshed");
        }
    
        async _checkTokeHasExpired() {
            let refreshed;
    
            let minValidity = this.MIN_VALIDITY + this.CHECK_INTERVAL;
            if (this.DEBUG) {
                console.log(`Updating token if not valid for at least ${minValidity}s`);
            }
            try {
                refreshed = await this._keycloak.updateToken(minValidity);
            } catch (error) {
                console.log('Failed to refresh the token', error);
            }
    
            if (this.DEBUG && refreshed)
                console.log("token has been refreshed");
        }
    
        async _onAuthSuccess() {
            // We check every once in a while if the token is still valid and
            // and refresh it if needed.
            if (this._checkId !== null) {
                clearInterval(this._checkId);
                this._checkId = null;
            }
            this._checkId = setInterval(this._checkTokeHasExpired.bind(this), this.CHECK_INTERVAL * 1000);
    
            this._onChanged();
        }
    
        async _onAuthLogout() {
            if (this._checkId !== null) {
                clearInterval(this._checkId);
                this._checkId = null;
            }
    
            this._onChanged();
        }
    
        async _ensureInstance() {
            if (this._keycloak !== null)
                return;
    
            const Keycloak = await importKeycloak(this._baseURL);
    
            this._keycloak = Keycloak({
                url: this._baseURL,
                realm: this._realm,
                clientId: this._clientId,
            });
    
            this._keycloak.onTokenExpired = this._onTokenExpired.bind(this);
            this._keycloak.onAuthRefreshSuccess = this._onChanged.bind(this);
            this._keycloak.onAuthRefreshError = this._onChanged.bind(this);
            this._keycloak.onAuthLogout = this._onAuthLogout.bind(this);
            this._keycloak.onAuthSuccess = this._onAuthSuccess.bind(this);
            this._keycloak.onAuthError = this._onChanged.bind(this);
            this._keycloak.onReady = this._onReady.bind(this);
        }
    
        async _keycloakInit(options) {
            // https://gitlab.tugraz.at/dbp/apps/library/issues/41
            // retry the keycloak init in case it fails, maybe it helps :/
            try {
                return await this._keycloak.init(options);
            } catch (e) {
                return await this._keycloak.init(options);
            }
        }
    
        async _ensureInit() {
            await this._ensureInstance();
            if (this._initDone)
                return;
            this._initDone = true;
    
            const options = {
                promiseType: 'native',
                pkceMethod: 'S256',
            };
    
            if (this.DEBUG) {
                options['enableLogging'] = true;
            }
    
            if (this._silentCheckSsoUri) {
    
                options['onLoad'] = 'check-sso';
                options['silentCheckSsoRedirectUri'] = ensureURL(this._silentCheckSsoUri);
    
                // When silent-sso-check is active but the iframe doesn't load/work we will
                // never return here, so add a timeout and emit a signal so the app can continue
                await promiseTimeout(5000, this._keycloakInit(options)).catch(() => {
                    console.log('Login timed out');
                    this._onChanged();
                });
            } else {
                await this._keycloakInit(options);
            }
    
        }
    
        /**
         * If this returns true you need to call login() at one point to finish the login process.
         */
        isLoggingIn() {
            const href = window.location.href;
            return (href.search('[&#]state=') >= 0 && href.search('[&#]session_state=') >= 0);
        }
    
        /**
         * Logs the user in. Might lead to a site refresh or the user needing to authenticate.
         *
         * @param {object} options
         * @param {string} [options.lang] - The locale to use on the keycloak login page
         */
        async login(options) {
            await this._ensureInit();
    
            options = options || {};
            const language = options['lang'] || 'en';
            const scope = options['scope'] || '';
    
            if (!this._keycloak.authenticated) {
                await this._keycloak.login({
                    kcLocale: language,  // Keycloak < 9.0
                    locale: language,
                    scope: scope,
                    idpHint: this._idpHint,
                });
            }
        }
    
        /**
         * Logs the user in if it is possible without leaving the page or the user needing to authenticate again.
         */
        async tryLogin() {
            await this._ensureInit();
        }
    
        /**
         * Logs the user out locally, but not with keycloak. Login will instantly log the user back in without
         * requiring a re-auth.
         */
        async localLogout() {
            this._keycloak.clearToken();
        }
    
        /**
         * Log the user out from keycloak.
         */
        async logout() {
            await this._ensureInit();
            this._keycloak.logout();
        }
    }