From a42e3257c21499c951d46e98a36fa9a10adde4c7 Mon Sep 17 00:00:00 2001 From: pbernet Date: Sun, 20 Oct 2024 11:20:45 +0200 Subject: [PATCH] Migrate OIDCKeycloak example to Keycloak 26.x --- build.sbt | 4 +- src/main/resources/KeycloakClient.html | 139 +- src/main/resources/js/keycloak.js | 1825 +++++++++++++++++ .../scala/akkahttp/oidc/OIDCKeycloak.scala | 11 +- 4 files changed, 1911 insertions(+), 68 deletions(-) create mode 100644 src/main/resources/js/keycloak.js diff --git a/build.sbt b/build.sbt index fa354e15..a289bdab 100644 --- a/build.sbt +++ b/build.sbt @@ -15,7 +15,7 @@ val kafkaVersion = "3.7.0" val activemqVersion = "5.18.5" // We are stuck with 5.x val artemisVersion = "2.37.0" val testContainersVersion = "1.20.1" -val keycloakVersion = "24.0.4" +val keycloakVersion = "26.0.1" val sttpVersion = "3.9.0" val influxdbVersion = "7.1.0" val awsClientVersion = "2.25.32" @@ -128,7 +128,7 @@ libraryDependencies ++= Seq( "com.crobox.clickhouse" %% "client" % "1.2.2", "org.opensearch" % "opensearch-testcontainers" % "2.0.1", - "com.github.dasniko" % "testcontainers-keycloak" % "3.3.1", + "com.github.dasniko" % "testcontainers-keycloak" % "3.5.1", "eu.rekawek.toxiproxy" % "toxiproxy-java" % "2.1.7", "org.testcontainers" % "junit-jupiter" % testContainersVersion % Test, "org.junit.jupiter" % "junit-jupiter-engine" % "5.9.2" % Test, diff --git a/src/main/resources/KeycloakClient.html b/src/main/resources/KeycloakClient.html index 680cce3a..b0e93c83 100644 --- a/src/main/resources/KeycloakClient.html +++ b/src/main/resources/KeycloakClient.html @@ -1,12 +1,13 @@ - + - - Keycloak test page + + Keycloak test client @@ -37,13 +38,39 @@

Result

-

+

 
 

Events

-

+

 
 
 
 
 
\ No newline at end of file
diff --git a/src/main/resources/js/keycloak.js b/src/main/resources/js/keycloak.js
new file mode 100644
index 00000000..53138151
--- /dev/null
+++ b/src/main/resources/js/keycloak.js
@@ -0,0 +1,1825 @@
+/*
+ * Copyright 2016 Red Hat, Inc. 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.
+ */
+function Keycloak(config) {
+    if (!(this instanceof Keycloak)) {
+        throw new Error("The 'Keycloak' constructor must be invoked with 'new'.")
+    }
+
+    if (typeof config !== 'string' && !isObject(config)) {
+        throw new Error("The 'Keycloak' constructor must be provided with a configuration object, or a URL to a JSON configuration file.");
+    }
+
+    if (isObject(config)) {
+        const requiredProperties = 'oidcProvider' in config
+            ? ['clientId']
+            : ['url', 'realm', 'clientId'];
+
+        for (const property of requiredProperties) {
+            if (!config[property]) {
+                throw new Error(`The configuration object is missing the required '${property}' property.`);
+            }
+        }
+    }
+
+    var kc = this;
+    var adapter;
+    var refreshQueue = [];
+    var callbackStorage;
+
+    var loginIframe = {
+        enable: true,
+        callbackList: [],
+        interval: 5
+    };
+
+    kc.didInitialize = false;
+
+    var useNonce = true;
+    var logInfo = createLogger(console.info);
+    var logWarn = createLogger(console.warn);
+
+    if (!globalThis.isSecureContext) {
+        logWarn(
+            "[KEYCLOAK] Keycloak JS must be used in a 'secure context' to function properly as it relies on browser APIs that are otherwise not available.\n" +
+            "Continuing to run your application insecurely will lead to unexpected behavior and breakage.\n\n" +
+            "For more information see: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
+        );
+    }
+
+    kc.init = function (initOptions = {}) {
+        if (kc.didInitialize) {
+            throw new Error("A 'Keycloak' instance can only be initialized once.");
+        }
+
+        kc.didInitialize = true;
+
+        kc.authenticated = false;
+
+        callbackStorage = createCallbackStorage();
+        var adapters = ['default', 'cordova', 'cordova-native'];
+
+        if (adapters.indexOf(initOptions.adapter) > -1) {
+            adapter = loadAdapter(initOptions.adapter);
+        } else if (typeof initOptions.adapter === "object") {
+            adapter = initOptions.adapter;
+        } else {
+            if (window.Cordova || window.cordova) {
+                adapter = loadAdapter('cordova');
+            } else {
+                adapter = loadAdapter();
+            }
+        }
+
+        if (typeof initOptions.useNonce !== 'undefined') {
+            useNonce = initOptions.useNonce;
+        }
+
+        if (typeof initOptions.checkLoginIframe !== 'undefined') {
+            loginIframe.enable = initOptions.checkLoginIframe;
+        }
+
+        if (initOptions.checkLoginIframeInterval) {
+            loginIframe.interval = initOptions.checkLoginIframeInterval;
+        }
+
+        if (initOptions.onLoad === 'login-required') {
+            kc.loginRequired = true;
+        }
+
+        if (initOptions.responseMode) {
+            if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {
+                kc.responseMode = initOptions.responseMode;
+            } else {
+                throw 'Invalid value for responseMode';
+            }
+        }
+
+        if (initOptions.flow) {
+            switch (initOptions.flow) {
+                case 'standard':
+                    kc.responseType = 'code';
+                    break;
+                case 'implicit':
+                    kc.responseType = 'id_token token';
+                    break;
+                case 'hybrid':
+                    kc.responseType = 'code id_token token';
+                    break;
+                default:
+                    throw 'Invalid value for flow';
+            }
+            kc.flow = initOptions.flow;
+        }
+
+        if (initOptions.timeSkew != null) {
+            kc.timeSkew = initOptions.timeSkew;
+        }
+
+        if (initOptions.redirectUri) {
+            kc.redirectUri = initOptions.redirectUri;
+        }
+
+        if (initOptions.silentCheckSsoRedirectUri) {
+            kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri;
+        }
+
+        if (typeof initOptions.silentCheckSsoFallback === 'boolean') {
+            kc.silentCheckSsoFallback = initOptions.silentCheckSsoFallback;
+        } else {
+            kc.silentCheckSsoFallback = true;
+        }
+
+        if (typeof initOptions.pkceMethod !== "undefined") {
+            if (initOptions.pkceMethod !== "S256" && initOptions.pkceMethod !== false) {
+                throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${initOptions.pkceMethod}.`);
+            }
+
+            kc.pkceMethod = initOptions.pkceMethod;
+        } else {
+            kc.pkceMethod = "S256";
+        }
+
+        if (typeof initOptions.enableLogging === 'boolean') {
+            kc.enableLogging = initOptions.enableLogging;
+        } else {
+            kc.enableLogging = false;
+        }
+
+        if (initOptions.logoutMethod === 'POST') {
+            kc.logoutMethod = 'POST';
+        } else {
+            kc.logoutMethod = 'GET';
+        }
+
+        if (typeof initOptions.scope === 'string') {
+            kc.scope = initOptions.scope;
+        }
+
+        if (typeof initOptions.acrValues === 'string') {
+            kc.acrValues = initOptions.acrValues;
+        }
+
+        if (typeof initOptions.messageReceiveTimeout === 'number' && initOptions.messageReceiveTimeout > 0) {
+            kc.messageReceiveTimeout = initOptions.messageReceiveTimeout;
+        } else {
+            kc.messageReceiveTimeout = 10000;
+        }
+
+        if (!kc.responseMode) {
+            kc.responseMode = 'fragment';
+        }
+        if (!kc.responseType) {
+            kc.responseType = 'code';
+            kc.flow = 'standard';
+        }
+
+        var promise = createPromise();
+
+        var initPromise = createPromise();
+        initPromise.promise.then(function () {
+            kc.onReady && kc.onReady(kc.authenticated);
+            promise.setSuccess(kc.authenticated);
+        }).catch(function (error) {
+            promise.setError(error);
+        });
+
+        var configPromise = loadConfig();
+
+        function onLoad() {
+            var doLogin = function (prompt) {
+                if (!prompt) {
+                    options.prompt = 'none';
+                }
+
+                if (initOptions.locale) {
+                    options.locale = initOptions.locale;
+                }
+                kc.login(options).then(function () {
+                    initPromise.setSuccess();
+                }).catch(function (error) {
+                    initPromise.setError(error);
+                });
+            }
+
+            var checkSsoSilently = async function () {
+                var ifrm = document.createElement("iframe");
+                var src = await kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri});
+                ifrm.setAttribute("src", src);
+                ifrm.setAttribute("sandbox", "allow-storage-access-by-user-activation allow-scripts allow-same-origin");
+                ifrm.setAttribute("title", "keycloak-silent-check-sso");
+                ifrm.style.display = "none";
+                document.body.appendChild(ifrm);
+
+                var messageCallback = function (event) {
+                    if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) {
+                        return;
+                    }
+
+                    var oauth = parseCallback(event.data);
+                    processCallback(oauth, initPromise);
+
+                    document.body.removeChild(ifrm);
+                    window.removeEventListener("message", messageCallback);
+                };
+
+                window.addEventListener("message", messageCallback);
+            };
+
+            var options = {};
+            switch (initOptions.onLoad) {
+                case 'check-sso':
+                    if (loginIframe.enable) {
+                        setupCheckLoginIframe().then(function () {
+                            checkLoginIframe().then(function (unchanged) {
+                                if (!unchanged) {
+                                    kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
+                                } else {
+                                    initPromise.setSuccess();
+                                }
+                            }).catch(function (error) {
+                                initPromise.setError(error);
+                            });
+                        });
+                    } else {
+                        kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false);
+                    }
+                    break;
+                case 'login-required':
+                    doLogin(true);
+                    break;
+                default:
+                    throw 'Invalid value for onLoad';
+            }
+        }
+
+        function processInit() {
+            var callback = parseCallback(window.location.href);
+
+            if (callback) {
+                window.history.replaceState(window.history.state, null, callback.newUrl);
+            }
+
+            if (callback && callback.valid) {
+                return setupCheckLoginIframe().then(function () {
+                    processCallback(callback, initPromise);
+                }).catch(function (error) {
+                    initPromise.setError(error);
+                });
+            }
+
+            if (initOptions.token && initOptions.refreshToken) {
+                setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
+
+                if (loginIframe.enable) {
+                    setupCheckLoginIframe().then(function () {
+                        checkLoginIframe().then(function (unchanged) {
+                            if (unchanged) {
+                                kc.onAuthSuccess && kc.onAuthSuccess();
+                                initPromise.setSuccess();
+                                scheduleCheckIframe();
+                            } else {
+                                initPromise.setSuccess();
+                            }
+                        }).catch(function (error) {
+                            initPromise.setError(error);
+                        });
+                    });
+                } else {
+                    kc.updateToken(-1).then(function () {
+                        kc.onAuthSuccess && kc.onAuthSuccess();
+                        initPromise.setSuccess();
+                    }).catch(function (error) {
+                        kc.onAuthError && kc.onAuthError();
+                        if (initOptions.onLoad) {
+                            onLoad();
+                        } else {
+                            initPromise.setError(error);
+                        }
+                    });
+                }
+            } else if (initOptions.onLoad) {
+                onLoad();
+            } else {
+                initPromise.setSuccess();
+            }
+        }
+
+        configPromise.then(function () {
+            check3pCookiesSupported()
+                .then(processInit)
+                .catch(function (error) {
+                    promise.setError(error);
+                });
+        });
+        configPromise.catch(function (error) {
+            promise.setError(error);
+        });
+
+        return promise.promise;
+    }
+
+    kc.login = function (options) {
+        return adapter.login(options);
+    }
+
+    function generateRandomData(len) {
+        if (typeof crypto === "undefined" || typeof crypto.getRandomValues === "undefined") {
+            throw new Error("Web Crypto API is not available.");
+        }
+
+        return crypto.getRandomValues(new Uint8Array(len));
+    }
+
+    function generateCodeVerifier(len) {
+        return generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
+    }
+
+    function generateRandomString(len, alphabet) {
+        var randomData = generateRandomData(len);
+        var chars = new Array(len);
+        for (var i = 0; i < len; i++) {
+            chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length);
+        }
+        return String.fromCharCode.apply(null, chars);
+    }
+
+    async function generatePkceChallenge(pkceMethod, codeVerifier) {
+        if (pkceMethod !== "S256") {
+            throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`);
+        }
+
+        // hash codeVerifier, then encode as url-safe base64 without padding
+        const hashBytes = new Uint8Array(await sha256Digest(codeVerifier));
+        const encodedHash = bytesToBase64(hashBytes)
+            .replace(/\+/g, '-')
+            .replace(/\//g, '_')
+            .replace(/\=/g, '');
+
+        return encodedHash;
+    }
+
+    function buildClaimsParameter(requestedAcr) {
+        var claims = {
+            id_token: {
+                acr: requestedAcr
+            }
+        }
+        return JSON.stringify(claims);
+    }
+
+    kc.createLoginUrl = async function (options) {
+        var state = createUUID();
+        var nonce = createUUID();
+
+        var redirectUri = adapter.redirectUri(options);
+
+        var callbackState = {
+            state: state,
+            nonce: nonce,
+            redirectUri: encodeURIComponent(redirectUri),
+            loginOptions: options
+        };
+
+        if (options && options.prompt) {
+            callbackState.prompt = options.prompt;
+        }
+
+        var baseUrl;
+        if (options && options.action == 'register') {
+            baseUrl = kc.endpoints.register();
+        } else {
+            baseUrl = kc.endpoints.authorize();
+        }
+
+        var scope = options && options.scope || kc.scope;
+        if (!scope) {
+            // if scope is not set, default to "openid"
+            scope = "openid";
+        } else if (scope.indexOf("openid") === -1) {
+            // if openid scope is missing, prefix the given scopes with it
+            scope = "openid " + scope;
+        }
+
+        var url = baseUrl
+            + '?client_id=' + encodeURIComponent(kc.clientId)
+            + '&redirect_uri=' + encodeURIComponent(redirectUri)
+            + '&state=' + encodeURIComponent(state)
+            + '&response_mode=' + encodeURIComponent(kc.responseMode)
+            + '&response_type=' + encodeURIComponent(kc.responseType)
+            + '&scope=' + encodeURIComponent(scope);
+        if (useNonce) {
+            url = url + '&nonce=' + encodeURIComponent(nonce);
+        }
+
+        if (options && options.prompt) {
+            url += '&prompt=' + encodeURIComponent(options.prompt);
+        }
+
+        if (options && typeof options.maxAge === 'number') {
+            url += '&max_age=' + encodeURIComponent(options.maxAge);
+        }
+
+        if (options && options.loginHint) {
+            url += '&login_hint=' + encodeURIComponent(options.loginHint);
+        }
+
+        if (options && options.idpHint) {
+            url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint);
+        }
+
+        if (options && options.action && options.action != 'register') {
+            url += '&kc_action=' + encodeURIComponent(options.action);
+        }
+
+        if (options && options.locale) {
+            url += '&ui_locales=' + encodeURIComponent(options.locale);
+        }
+
+        if (options && options.acr) {
+            var claimsParameter = buildClaimsParameter(options.acr);
+            url += '&claims=' + encodeURIComponent(claimsParameter);
+        }
+
+        if ((options && options.acrValues) || kc.acrValues) {
+            url += '&acr_values=' + encodeURIComponent(options.acrValues || kc.acrValues);
+        }
+
+        if (kc.pkceMethod) {
+            try {
+                const codeVerifier = generateCodeVerifier(96);
+                const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier);
+
+                callbackState.pkceCodeVerifier = codeVerifier;
+
+                url += '&code_challenge=' + pkceChallenge;
+                url += '&code_challenge_method=' + kc.pkceMethod;
+            } catch (error) {
+                throw new Error("Failed to generate PKCE challenge.", {cause: error});
+            }
+        }
+
+        callbackStorage.add(callbackState);
+
+        return url;
+    }
+
+    kc.logout = function (options) {
+        return adapter.logout(options);
+    }
+
+    kc.createLogoutUrl = function (options) {
+
+        const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
+        if (logoutMethod === 'POST') {
+            return kc.endpoints.logout();
+        }
+
+        var url = kc.endpoints.logout()
+            + '?client_id=' + encodeURIComponent(kc.clientId)
+            + '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
+
+        if (kc.idToken) {
+            url += '&id_token_hint=' + encodeURIComponent(kc.idToken);
+        }
+
+        return url;
+    }
+
+    kc.register = function (options) {
+        return adapter.register(options);
+    }
+
+    kc.createRegisterUrl = async function (options) {
+        if (!options) {
+            options = {};
+        }
+        options.action = 'register';
+        return await kc.createLoginUrl(options);
+    }
+
+    kc.createAccountUrl = function (options) {
+        var realm = getRealmUrl();
+        var url = undefined;
+        if (typeof realm !== 'undefined') {
+            url = realm
+                + '/account'
+                + '?referrer=' + encodeURIComponent(kc.clientId)
+                + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
+        }
+        return url;
+    }
+
+    kc.accountManagement = function () {
+        return adapter.accountManagement();
+    }
+
+    kc.hasRealmRole = function (role) {
+        var access = kc.realmAccess;
+        return !!access && access.roles.indexOf(role) >= 0;
+    }
+
+    kc.hasResourceRole = function (role, resource) {
+        if (!kc.resourceAccess) {
+            return false;
+        }
+
+        var access = kc.resourceAccess[resource || kc.clientId];
+        return !!access && access.roles.indexOf(role) >= 0;
+    }
+
+    kc.loadUserProfile = function () {
+        var url = getRealmUrl() + '/account';
+        var req = new XMLHttpRequest();
+        req.open('GET', url, true);
+        req.setRequestHeader('Accept', 'application/json');
+        req.setRequestHeader('Authorization', 'bearer ' + kc.token);
+
+        var promise = createPromise();
+
+        req.onreadystatechange = function () {
+            if (req.readyState == 4) {
+                if (req.status == 200) {
+                    kc.profile = JSON.parse(req.responseText);
+                    promise.setSuccess(kc.profile);
+                } else {
+                    promise.setError();
+                }
+            }
+        }
+
+        req.send();
+
+        return promise.promise;
+    }
+
+    kc.loadUserInfo = function () {
+        var url = kc.endpoints.userinfo();
+        var req = new XMLHttpRequest();
+        req.open('GET', url, true);
+        req.setRequestHeader('Accept', 'application/json');
+        req.setRequestHeader('Authorization', 'bearer ' + kc.token);
+
+        var promise = createPromise();
+
+        req.onreadystatechange = function () {
+            if (req.readyState == 4) {
+                if (req.status == 200) {
+                    kc.userInfo = JSON.parse(req.responseText);
+                    promise.setSuccess(kc.userInfo);
+                } else {
+                    promise.setError();
+                }
+            }
+        }
+
+        req.send();
+
+        return promise.promise;
+    }
+
+    kc.isTokenExpired = function (minValidity) {
+        if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit')) {
+            throw 'Not authenticated';
+        }
+
+        if (kc.timeSkew == null) {
+            logInfo('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set');
+            return true;
+        }
+
+        var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew;
+        if (minValidity) {
+            if (isNaN(minValidity)) {
+                throw 'Invalid minValidity';
+            }
+            expiresIn -= minValidity;
+        }
+        return expiresIn < 0;
+    }
+
+    kc.updateToken = function (minValidity) {
+        var promise = createPromise();
+
+        if (!kc.refreshToken) {
+            promise.setError();
+            return promise.promise;
+        }
+
+        minValidity = minValidity || 5;
+
+        var exec = function () {
+            var refreshToken = false;
+            if (minValidity == -1) {
+                refreshToken = true;
+                logInfo('[KEYCLOAK] Refreshing token: forced refresh');
+            } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) {
+                refreshToken = true;
+                logInfo('[KEYCLOAK] Refreshing token: token expired');
+            }
+
+            if (!refreshToken) {
+                promise.setSuccess(false);
+            } else {
+                var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
+                var url = kc.endpoints.token();
+
+                refreshQueue.push(promise);
+
+                if (refreshQueue.length == 1) {
+                    var req = new XMLHttpRequest();
+                    req.open('POST', url, true);
+                    req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+                    req.withCredentials = true;
+
+                    params += '&client_id=' + encodeURIComponent(kc.clientId);
+
+                    var timeLocal = new Date().getTime();
+
+                    req.onreadystatechange = function () {
+                        if (req.readyState == 4) {
+                            if (req.status == 200) {
+                                logInfo('[KEYCLOAK] Token refreshed');
+
+                                timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+                                var tokenResponse = JSON.parse(req.responseText);
+
+                                setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal);
+
+                                kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess();
+                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
+                                    p.setSuccess(true);
+                                }
+                            } else {
+                                logWarn('[KEYCLOAK] Failed to refresh token');
+
+                                if (req.status == 400) {
+                                    kc.clearToken();
+                                }
+
+                                kc.onAuthRefreshError && kc.onAuthRefreshError();
+                                for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
+                                    p.setError(true);
+                                }
+                            }
+                        }
+                    };
+
+                    req.send(params);
+                }
+            }
+        }
+
+        if (loginIframe.enable) {
+            var iframePromise = checkLoginIframe();
+            iframePromise.then(function () {
+                exec();
+            }).catch(function (error) {
+                promise.setError(error);
+            });
+        } else {
+            exec();
+        }
+
+        return promise.promise;
+    }
+
+    kc.clearToken = function () {
+        if (kc.token) {
+            setToken(null, null, null);
+            kc.onAuthLogout && kc.onAuthLogout();
+            if (kc.loginRequired) {
+                kc.login();
+            }
+        }
+    }
+
+    function getRealmUrl() {
+        if (typeof kc.authServerUrl !== 'undefined') {
+            if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
+                return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
+            } else {
+                return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
+            }
+        } else {
+            return undefined;
+        }
+    }
+
+    function getOrigin() {
+        if (!window.location.origin) {
+            return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
+        } else {
+            return window.location.origin;
+        }
+    }
+
+    function processCallback(oauth, promise) {
+        var code = oauth.code;
+        var error = oauth.error;
+        var prompt = oauth.prompt;
+
+        var timeLocal = new Date().getTime();
+
+        if (oauth['kc_action_status']) {
+            kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status'], oauth['kc_action']);
+        }
+
+        if (error) {
+            if (prompt != 'none') {
+                if (oauth.error_description && oauth.error_description === "authentication_expired") {
+                    kc.login(oauth.loginOptions);
+                } else {
+                    var errorData = {error: error, error_description: oauth.error_description};
+                    kc.onAuthError && kc.onAuthError(errorData);
+                    promise && promise.setError(errorData);
+                }
+            } else {
+                promise && promise.setSuccess();
+            }
+            return;
+        } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) {
+            authSuccess(oauth.access_token, null, oauth.id_token, true);
+        }
+
+        if ((kc.flow != 'implicit') && code) {
+            var params = 'code=' + code + '&grant_type=authorization_code';
+            var url = kc.endpoints.token();
+
+            var req = new XMLHttpRequest();
+            req.open('POST', url, true);
+            req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+
+            params += '&client_id=' + encodeURIComponent(kc.clientId);
+            params += '&redirect_uri=' + oauth.redirectUri;
+
+            if (oauth.pkceCodeVerifier) {
+                params += '&code_verifier=' + oauth.pkceCodeVerifier;
+            }
+
+            req.withCredentials = true;
+
+            req.onreadystatechange = function () {
+                if (req.readyState == 4) {
+                    if (req.status == 200) {
+
+                        var tokenResponse = JSON.parse(req.responseText);
+                        authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard');
+                        scheduleCheckIframe();
+                    } else {
+                        kc.onAuthError && kc.onAuthError();
+                        promise && promise.setError();
+                    }
+                }
+            };
+
+            req.send(params);
+        }
+
+        function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) {
+            timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+            setToken(accessToken, refreshToken, idToken, timeLocal);
+
+            if (useNonce && (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
+                logInfo('[KEYCLOAK] Invalid nonce, clearing token');
+                kc.clearToken();
+                promise && promise.setError();
+            } else {
+                if (fulfillPromise) {
+                    kc.onAuthSuccess && kc.onAuthSuccess();
+                    promise && promise.setSuccess();
+                }
+            }
+        }
+
+    }
+
+    function loadConfig() {
+        var promise = createPromise();
+        var configUrl;
+
+        if (typeof config === 'string') {
+            configUrl = config;
+        }
+
+        function setupOidcEndoints(oidcConfiguration) {
+            if (!oidcConfiguration) {
+                kc.endpoints = {
+                    authorize: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/auth';
+                    },
+                    token: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/token';
+                    },
+                    logout: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/logout';
+                    },
+                    checkSessionIframe: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
+                    },
+                    thirdPartyCookiesIframe: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html';
+                    },
+                    register: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/registrations';
+                    },
+                    userinfo: function () {
+                        return getRealmUrl() + '/protocol/openid-connect/userinfo';
+                    }
+                };
+            } else {
+                kc.endpoints = {
+                    authorize: function () {
+                        return oidcConfiguration.authorization_endpoint;
+                    },
+                    token: function () {
+                        return oidcConfiguration.token_endpoint;
+                    },
+                    logout: function () {
+                        if (!oidcConfiguration.end_session_endpoint) {
+                            throw "Not supported by the OIDC server";
+                        }
+                        return oidcConfiguration.end_session_endpoint;
+                    },
+                    checkSessionIframe: function () {
+                        if (!oidcConfiguration.check_session_iframe) {
+                            throw "Not supported by the OIDC server";
+                        }
+                        return oidcConfiguration.check_session_iframe;
+                    },
+                    register: function () {
+                        throw 'Redirection to "Register user" page not supported in standard OIDC mode';
+                    },
+                    userinfo: function () {
+                        if (!oidcConfiguration.userinfo_endpoint) {
+                            throw "Not supported by the OIDC server";
+                        }
+                        return oidcConfiguration.userinfo_endpoint;
+                    }
+                }
+            }
+        }
+
+        if (configUrl) {
+            var req = new XMLHttpRequest();
+            req.open('GET', configUrl, true);
+            req.setRequestHeader('Accept', 'application/json');
+
+            req.onreadystatechange = function () {
+                if (req.readyState == 4) {
+                    if (req.status == 200 || fileLoaded(req)) {
+                        var config = JSON.parse(req.responseText);
+
+                        kc.authServerUrl = config['auth-server-url'];
+                        kc.realm = config['realm'];
+                        kc.clientId = config['resource'];
+                        setupOidcEndoints(null);
+                        promise.setSuccess();
+                    } else {
+                        promise.setError();
+                    }
+                }
+            };
+
+            req.send();
+        } else {
+            kc.clientId = config.clientId;
+
+            var oidcProvider = config['oidcProvider'];
+            if (!oidcProvider) {
+                kc.authServerUrl = config.url;
+                kc.realm = config.realm;
+                setupOidcEndoints(null);
+                promise.setSuccess();
+            } else {
+                if (typeof oidcProvider === 'string') {
+                    var oidcProviderConfigUrl;
+                    if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
+                        oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
+                    } else {
+                        oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
+                    }
+                    var req = new XMLHttpRequest();
+                    req.open('GET', oidcProviderConfigUrl, true);
+                    req.setRequestHeader('Accept', 'application/json');
+
+                    req.onreadystatechange = function () {
+                        if (req.readyState == 4) {
+                            if (req.status == 200 || fileLoaded(req)) {
+                                var oidcProviderConfig = JSON.parse(req.responseText);
+                                setupOidcEndoints(oidcProviderConfig);
+                                promise.setSuccess();
+                            } else {
+                                promise.setError();
+                            }
+                        }
+                    };
+
+                    req.send();
+                } else {
+                    setupOidcEndoints(oidcProvider);
+                    promise.setSuccess();
+                }
+            }
+        }
+
+        return promise.promise;
+    }
+
+    function fileLoaded(xhr) {
+        return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:');
+    }
+
+    function setToken(token, refreshToken, idToken, timeLocal) {
+        if (kc.tokenTimeoutHandle) {
+            clearTimeout(kc.tokenTimeoutHandle);
+            kc.tokenTimeoutHandle = null;
+        }
+
+        if (refreshToken) {
+            kc.refreshToken = refreshToken;
+            kc.refreshTokenParsed = decodeToken(refreshToken);
+        } else {
+            delete kc.refreshToken;
+            delete kc.refreshTokenParsed;
+        }
+
+        if (idToken) {
+            kc.idToken = idToken;
+            kc.idTokenParsed = decodeToken(idToken);
+        } else {
+            delete kc.idToken;
+            delete kc.idTokenParsed;
+        }
+
+        if (token) {
+            kc.token = token;
+            kc.tokenParsed = decodeToken(token);
+            kc.sessionId = kc.tokenParsed.sid;
+            kc.authenticated = true;
+            kc.subject = kc.tokenParsed.sub;
+            kc.realmAccess = kc.tokenParsed.realm_access;
+            kc.resourceAccess = kc.tokenParsed.resource_access;
+
+            if (timeLocal) {
+                kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
+            }
+
+            if (kc.timeSkew != null) {
+                logInfo('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds');
+
+                if (kc.onTokenExpired) {
+                    var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000;
+                    logInfo('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s');
+                    if (expiresIn <= 0) {
+                        kc.onTokenExpired();
+                    } else {
+                        kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn);
+                    }
+                }
+            }
+        } else {
+            delete kc.token;
+            delete kc.tokenParsed;
+            delete kc.subject;
+            delete kc.realmAccess;
+            delete kc.resourceAccess;
+
+            kc.authenticated = false;
+        }
+    }
+
+    function createUUID() {
+        if (typeof crypto === "undefined" || typeof crypto.randomUUID === "undefined") {
+            throw new Error("Web Crypto API is not available.");
+        }
+
+        return crypto.randomUUID();
+    }
+
+    function parseCallback(url) {
+        var oauth = parseCallbackUrl(url);
+        if (!oauth) {
+            return;
+        }
+
+        var oauthState = callbackStorage.get(oauth.state);
+
+        if (oauthState) {
+            oauth.valid = true;
+            oauth.redirectUri = oauthState.redirectUri;
+            oauth.storedNonce = oauthState.nonce;
+            oauth.prompt = oauthState.prompt;
+            oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier;
+            oauth.loginOptions = oauthState.loginOptions;
+        }
+
+        return oauth;
+    }
+
+    function parseCallbackUrl(url) {
+        var supportedParams;
+        switch (kc.flow) {
+            case 'standard':
+                supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss'];
+                break;
+            case 'implicit':
+                supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];
+                break;
+            case 'hybrid':
+                supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss'];
+                break;
+        }
+
+        supportedParams.push('error');
+        supportedParams.push('error_description');
+        supportedParams.push('error_uri');
+
+        var queryIndex = url.indexOf('?');
+        var fragmentIndex = url.indexOf('#');
+
+        var newUrl;
+        var parsed;
+
+        if (kc.responseMode === 'query' && queryIndex !== -1) {
+            newUrl = url.substring(0, queryIndex);
+            parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
+            if (parsed.paramsString !== '') {
+                newUrl += '?' + parsed.paramsString;
+            }
+            if (fragmentIndex !== -1) {
+                newUrl += url.substring(fragmentIndex);
+            }
+        } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
+            newUrl = url.substring(0, fragmentIndex);
+            parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
+            if (parsed.paramsString !== '') {
+                newUrl += '#' + parsed.paramsString;
+            }
+        }
+
+        if (parsed && parsed.oauthParams) {
+            if (kc.flow === 'standard' || kc.flow === 'hybrid') {
+                if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
+                    parsed.oauthParams.newUrl = newUrl;
+                    return parsed.oauthParams;
+                }
+            } else if (kc.flow === 'implicit') {
+                if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
+                    parsed.oauthParams.newUrl = newUrl;
+                    return parsed.oauthParams;
+                }
+            }
+        }
+    }
+
+    function parseCallbackParams(paramsString, supportedParams) {
+        var p = paramsString.split('&');
+        var result = {
+            paramsString: '',
+            oauthParams: {}
+        }
+        for (var i = 0; i < p.length; i++) {
+            var split = p[i].indexOf("=");
+            var key = p[i].slice(0, split);
+            if (supportedParams.indexOf(key) !== -1) {
+                result.oauthParams[key] = p[i].slice(split + 1);
+            } else {
+                if (result.paramsString !== '') {
+                    result.paramsString += '&';
+                }
+                result.paramsString += p[i];
+            }
+        }
+        return result;
+    }
+
+    function createPromise() {
+        // Need to create a native Promise which also preserves the
+        // interface of the custom promise type previously used by the API
+        var p = {
+            setSuccess: function (result) {
+                p.resolve(result);
+            },
+
+            setError: function (result) {
+                p.reject(result);
+            }
+        };
+        p.promise = new Promise(function (resolve, reject) {
+            p.resolve = resolve;
+            p.reject = reject;
+        });
+
+        return p;
+    }
+
+    // Function to extend existing native Promise with timeout
+    function applyTimeoutToPromise(promise, timeout, errorMessage) {
+        var timeoutHandle = null;
+        var timeoutPromise = new Promise(function (resolve, reject) {
+            timeoutHandle = setTimeout(function () {
+                reject({"error": errorMessage || "Promise is not settled within timeout of " + timeout + "ms"});
+            }, timeout);
+        });
+
+        return Promise.race([promise, timeoutPromise]).finally(function () {
+            clearTimeout(timeoutHandle);
+        });
+    }
+
+    function setupCheckLoginIframe() {
+        var promise = createPromise();
+
+        if (!loginIframe.enable) {
+            promise.setSuccess();
+            return promise.promise;
+        }
+
+        if (loginIframe.iframe) {
+            promise.setSuccess();
+            return promise.promise;
+        }
+
+        var iframe = document.createElement('iframe');
+        loginIframe.iframe = iframe;
+
+        iframe.onload = function () {
+            var authUrl = kc.endpoints.authorize();
+            if (authUrl.charAt(0) === '/') {
+                loginIframe.iframeOrigin = getOrigin();
+            } else {
+                loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
+            }
+            promise.setSuccess();
+        }
+
+        var src = kc.endpoints.checkSessionIframe();
+        iframe.setAttribute('src', src);
+        iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');
+        iframe.setAttribute('title', 'keycloak-session-iframe');
+        iframe.style.display = 'none';
+        document.body.appendChild(iframe);
+
+        var messageCallback = function (event) {
+            if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) {
+                return;
+            }
+
+            if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) {
+                return;
+            }
+
+
+            if (event.data != 'unchanged') {
+                kc.clearToken();
+            }
+
+            var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length);
+
+            for (var i = callbacks.length - 1; i >= 0; --i) {
+                var promise = callbacks[i];
+                if (event.data == 'error') {
+                    promise.setError();
+                } else {
+                    promise.setSuccess(event.data == 'unchanged');
+                }
+            }
+        };
+
+        window.addEventListener('message', messageCallback, false);
+
+        return promise.promise;
+    }
+
+    function scheduleCheckIframe() {
+        if (loginIframe.enable) {
+            if (kc.token) {
+                setTimeout(function () {
+                    checkLoginIframe().then(function (unchanged) {
+                        if (unchanged) {
+                            scheduleCheckIframe();
+                        }
+                    });
+                }, loginIframe.interval * 1000);
+            }
+        }
+    }
+
+    function checkLoginIframe() {
+        var promise = createPromise();
+
+        if (loginIframe.iframe && loginIframe.iframeOrigin) {
+            var msg = kc.clientId + ' ' + (kc.sessionId ? kc.sessionId : '');
+            loginIframe.callbackList.push(promise);
+            var origin = loginIframe.iframeOrigin;
+            if (loginIframe.callbackList.length == 1) {
+                loginIframe.iframe.contentWindow.postMessage(msg, origin);
+            }
+        } else {
+            promise.setSuccess();
+        }
+
+        return promise.promise;
+    }
+
+    function check3pCookiesSupported() {
+        var promise = createPromise();
+
+        if (loginIframe.enable || kc.silentCheckSsoRedirectUri) {
+            var iframe = document.createElement('iframe');
+            iframe.setAttribute('src', kc.endpoints.thirdPartyCookiesIframe());
+            iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin');
+            iframe.setAttribute('title', 'keycloak-3p-check-iframe');
+            iframe.style.display = 'none';
+            document.body.appendChild(iframe);
+
+            var messageCallback = function (event) {
+                if (iframe.contentWindow !== event.source) {
+                    return;
+                }
+
+                if (event.data !== "supported" && event.data !== "unsupported") {
+                    return;
+                } else if (event.data === "unsupported") {
+                    logWarn(
+                        "[KEYCLOAK] Your browser is blocking access to 3rd-party cookies, this means:\n\n" +
+                        " - It is not possible to retrieve tokens without redirecting to the Keycloak server (a.k.a. no support for silent authentication).\n" +
+                        " - It is not possible to automatically detect changes to the session status (such as the user logging out in another tab).\n\n" +
+                        "For more information see: https://www.keycloak.org/docs/latest/securing_apps/#_modern_browsers"
+                    );
+
+                    loginIframe.enable = false;
+                    if (kc.silentCheckSsoFallback) {
+                        kc.silentCheckSsoRedirectUri = false;
+                    }
+                }
+
+                document.body.removeChild(iframe);
+                window.removeEventListener("message", messageCallback);
+                promise.setSuccess();
+            };
+
+            window.addEventListener('message', messageCallback, false);
+        } else {
+            promise.setSuccess();
+        }
+
+        return applyTimeoutToPromise(promise.promise, kc.messageReceiveTimeout, "Timeout when waiting for 3rd party check iframe message.");
+    }
+
+    function loadAdapter(type) {
+        if (!type || type == 'default') {
+            return {
+                login: async function (options) {
+                    window.location.assign(await kc.createLoginUrl(options));
+                    return createPromise().promise;
+                },
+
+                logout: async function (options) {
+
+                    const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
+                    if (logoutMethod === "GET") {
+                        window.location.replace(kc.createLogoutUrl(options));
+                        return;
+                    }
+
+                    const logoutUrl = kc.createLogoutUrl(options);
+                    const response = await fetch(logoutUrl, {
+                        method: "POST",
+                        headers: {
+                            "Content-Type": "application/x-www-form-urlencoded"
+                        },
+                        body: new URLSearchParams({
+                            id_token_hint: kc.idToken,
+                            client_id: kc.clientId,
+                            post_logout_redirect_uri: adapter.redirectUri(options, false)
+                        })
+                    });
+
+                    if (response.redirected) {
+                        window.location.href = response.url;
+                        return;
+                    }
+
+                    if (response.ok) {
+                        window.location.reload();
+                        return;
+                    }
+
+                    throw new Error("Logout failed, request returned an error code.");
+                },
+
+                register: async function (options) {
+                    window.location.assign(await kc.createRegisterUrl(options));
+                    return createPromise().promise;
+                },
+
+                accountManagement: function () {
+                    var accountUrl = kc.createAccountUrl();
+                    if (typeof accountUrl !== 'undefined') {
+                        window.location.href = accountUrl;
+                    } else {
+                        throw "Not supported by the OIDC server";
+                    }
+                    return createPromise().promise;
+                },
+
+                redirectUri: function (options, encodeHash) {
+                    if (arguments.length == 1) {
+                        encodeHash = true;
+                    }
+
+                    if (options && options.redirectUri) {
+                        return options.redirectUri;
+                    } else if (kc.redirectUri) {
+                        return kc.redirectUri;
+                    } else {
+                        return location.href;
+                    }
+                }
+            };
+        }
+
+        if (type == 'cordova') {
+            loginIframe.enable = false;
+            var cordovaOpenWindowWrapper = function (loginUrl, target, options) {
+                if (window.cordova && window.cordova.InAppBrowser) {
+                    // Use inappbrowser for IOS and Android if available
+                    return window.cordova.InAppBrowser.open(loginUrl, target, options);
+                } else {
+                    return window.open(loginUrl, target, options);
+                }
+            };
+
+            var shallowCloneCordovaOptions = function (userOptions) {
+                if (userOptions && userOptions.cordovaOptions) {
+                    return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {
+                        options[optionName] = userOptions.cordovaOptions[optionName];
+                        return options;
+                    }, {});
+                } else {
+                    return {};
+                }
+            };
+
+            var formatCordovaOptions = function (cordovaOptions) {
+                return Object.keys(cordovaOptions).reduce(function (options, optionName) {
+                    options.push(optionName + "=" + cordovaOptions[optionName]);
+                    return options;
+                }, []).join(",");
+            };
+
+            var createCordovaOptions = function (userOptions) {
+                var cordovaOptions = shallowCloneCordovaOptions(userOptions);
+                cordovaOptions.location = 'no';
+                if (userOptions && userOptions.prompt == 'none') {
+                    cordovaOptions.hidden = 'yes';
+                }
+                return formatCordovaOptions(cordovaOptions);
+            };
+
+            var getCordovaRedirectUri = function () {
+                return kc.redirectUri || 'http://localhost';
+            }
+
+            return {
+                login: async function (options) {
+                    var promise = createPromise();
+
+                    var cordovaOptions = createCordovaOptions(options);
+                    var loginUrl = await kc.createLoginUrl(options);
+                    var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
+                    var completed = false;
+
+                    var closed = false;
+                    var closeBrowser = function () {
+                        closed = true;
+                        ref.close();
+                    };
+
+                    ref.addEventListener('loadstart', function (event) {
+                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                            var callback = parseCallback(event.url);
+                            processCallback(callback, promise);
+                            closeBrowser();
+                            completed = true;
+                        }
+                    });
+
+                    ref.addEventListener('loaderror', function (event) {
+                        if (!completed) {
+                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                                var callback = parseCallback(event.url);
+                                processCallback(callback, promise);
+                                closeBrowser();
+                                completed = true;
+                            } else {
+                                promise.setError();
+                                closeBrowser();
+                            }
+                        }
+                    });
+
+                    ref.addEventListener('exit', function (event) {
+                        if (!closed) {
+                            promise.setError({
+                                reason: "closed_by_user"
+                            });
+                        }
+                    });
+
+                    return promise.promise;
+                },
+
+                logout: function (options) {
+                    var promise = createPromise();
+
+                    var logoutUrl = kc.createLogoutUrl(options);
+                    var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes,clearcache=yes');
+
+                    var error;
+
+                    ref.addEventListener('loadstart', function (event) {
+                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                            ref.close();
+                        }
+                    });
+
+                    ref.addEventListener('loaderror', function (event) {
+                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                            ref.close();
+                        } else {
+                            error = true;
+                            ref.close();
+                        }
+                    });
+
+                    ref.addEventListener('exit', function (event) {
+                        if (error) {
+                            promise.setError();
+                        } else {
+                            kc.clearToken();
+                            promise.setSuccess();
+                        }
+                    });
+
+                    return promise.promise;
+                },
+
+                register: async function (options) {
+                    var promise = createPromise();
+                    var registerUrl = await kc.createRegisterUrl();
+                    var cordovaOptions = createCordovaOptions(options);
+                    var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
+                    ref.addEventListener('loadstart', function (event) {
+                        if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                            ref.close();
+                            var oauth = parseCallback(event.url);
+                            processCallback(oauth, promise);
+                        }
+                    });
+                    return promise.promise;
+                },
+
+                accountManagement: function () {
+                    var accountUrl = kc.createAccountUrl();
+                    if (typeof accountUrl !== 'undefined') {
+                        var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
+                        ref.addEventListener('loadstart', function (event) {
+                            if (event.url.indexOf(getCordovaRedirectUri()) == 0) {
+                                ref.close();
+                            }
+                        });
+                    } else {
+                        throw "Not supported by the OIDC server";
+                    }
+                },
+
+                redirectUri: function (options) {
+                    return getCordovaRedirectUri();
+                }
+            }
+        }
+
+        if (type == 'cordova-native') {
+            loginIframe.enable = false;
+
+            return {
+                login: async function (options) {
+                    var promise = createPromise();
+                    var loginUrl = await kc.createLoginUrl(options);
+
+                    universalLinks.subscribe('keycloak', function (event) {
+                        universalLinks.unsubscribe('keycloak');
+                        window.cordova.plugins.browsertab.close();
+                        var oauth = parseCallback(event.url);
+                        processCallback(oauth, promise);
+                    });
+
+                    window.cordova.plugins.browsertab.openUrl(loginUrl);
+                    return promise.promise;
+                },
+
+                logout: function (options) {
+                    var promise = createPromise();
+                    var logoutUrl = kc.createLogoutUrl(options);
+
+                    universalLinks.subscribe('keycloak', function (event) {
+                        universalLinks.unsubscribe('keycloak');
+                        window.cordova.plugins.browsertab.close();
+                        kc.clearToken();
+                        promise.setSuccess();
+                    });
+
+                    window.cordova.plugins.browsertab.openUrl(logoutUrl);
+                    return promise.promise;
+                },
+
+                register: async function (options) {
+                    var promise = createPromise();
+                    var registerUrl = await kc.createRegisterUrl(options);
+                    universalLinks.subscribe('keycloak', function (event) {
+                        universalLinks.unsubscribe('keycloak');
+                        window.cordova.plugins.browsertab.close();
+                        var oauth = parseCallback(event.url);
+                        processCallback(oauth, promise);
+                    });
+                    window.cordova.plugins.browsertab.openUrl(registerUrl);
+                    return promise.promise;
+
+                },
+
+                accountManagement: function () {
+                    var accountUrl = kc.createAccountUrl();
+                    if (typeof accountUrl !== 'undefined') {
+                        window.cordova.plugins.browsertab.openUrl(accountUrl);
+                    } else {
+                        throw "Not supported by the OIDC server";
+                    }
+                },
+
+                redirectUri: function (options) {
+                    if (options && options.redirectUri) {
+                        return options.redirectUri;
+                    } else if (kc.redirectUri) {
+                        return kc.redirectUri;
+                    } else {
+                        return "http://localhost";
+                    }
+                }
+            }
+        }
+
+        throw 'invalid adapter type: ' + type;
+    }
+
+    var LocalStorage = function () {
+        if (!(this instanceof LocalStorage)) {
+            return new LocalStorage();
+        }
+
+        localStorage.setItem('kc-test', 'test');
+        localStorage.removeItem('kc-test');
+
+        var cs = this;
+
+        function clearExpired() {
+            var time = new Date().getTime();
+            for (var i = 0; i < localStorage.length; i++) {
+                var key = localStorage.key(i);
+                if (key && key.indexOf('kc-callback-') == 0) {
+                    var value = localStorage.getItem(key);
+                    if (value) {
+                        try {
+                            var expires = JSON.parse(value).expires;
+                            if (!expires || expires < time) {
+                                localStorage.removeItem(key);
+                            }
+                        } catch (err) {
+                            localStorage.removeItem(key);
+                        }
+                    }
+                }
+            }
+        }
+
+        cs.get = function (state) {
+            if (!state) {
+                return;
+            }
+
+            var key = 'kc-callback-' + state;
+            var value = localStorage.getItem(key);
+            if (value) {
+                localStorage.removeItem(key);
+                value = JSON.parse(value);
+            }
+
+            clearExpired();
+            return value;
+        };
+
+        cs.add = function (state) {
+            clearExpired();
+
+            var key = 'kc-callback-' + state.state;
+            state.expires = new Date().getTime() + (60 * 60 * 1000);
+            localStorage.setItem(key, JSON.stringify(state));
+        };
+    };
+
+    var CookieStorage = function () {
+        if (!(this instanceof CookieStorage)) {
+            return new CookieStorage();
+        }
+
+        var cs = this;
+
+        cs.get = function (state) {
+            if (!state) {
+                return;
+            }
+
+            var value = getCookie('kc-callback-' + state);
+            setCookie('kc-callback-' + state, '', cookieExpiration(-100));
+            if (value) {
+                return JSON.parse(value);
+            }
+        };
+
+        cs.add = function (state) {
+            setCookie('kc-callback-' + state.state, JSON.stringify(state), cookieExpiration(60));
+        };
+
+        cs.removeItem = function (key) {
+            setCookie(key, '', cookieExpiration(-100));
+        };
+
+        var cookieExpiration = function (minutes) {
+            var exp = new Date();
+            exp.setTime(exp.getTime() + (minutes * 60 * 1000));
+            return exp;
+        };
+
+        var getCookie = function (key) {
+            var name = key + '=';
+            var ca = document.cookie.split(';');
+            for (var i = 0; i < ca.length; i++) {
+                var c = ca[i];
+                while (c.charAt(0) == ' ') {
+                    c = c.substring(1);
+                }
+                if (c.indexOf(name) == 0) {
+                    return c.substring(name.length, c.length);
+                }
+            }
+            return '';
+        };
+
+        var setCookie = function (key, value, expirationDate) {
+            var cookie = key + '=' + value + '; '
+                + 'expires=' + expirationDate.toUTCString() + '; ';
+            document.cookie = cookie;
+        }
+    };
+
+    function createCallbackStorage() {
+        try {
+            return new LocalStorage();
+        } catch (err) {
+        }
+
+        return new CookieStorage();
+    }
+
+    function createLogger(fn) {
+        return function () {
+            if (kc.enableLogging) {
+                fn.apply(console, Array.prototype.slice.call(arguments));
+            }
+        };
+    }
+}
+
+// Allow the Keycloak function to be used in both browser environments and module systems
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = Keycloak;
+} else {
+    window.Keycloak = Keycloak;
+}
+
+/**
+ * @param {ArrayBuffer} bytes
+ * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
+ */
+function bytesToBase64(bytes) {
+    const binString = String.fromCodePoint(...bytes);
+    return btoa(binString);
+}
+
+/**
+ * @param {string} message
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example
+ */
+async function sha256Digest(message) {
+    const encoder = new TextEncoder();
+    const data = encoder.encode(message);
+
+    if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") {
+        throw new Error("Web Crypto API is not available.");
+    }
+
+    return await crypto.subtle.digest("SHA-256", data);
+}
+
+/**
+ * @param {string} token
+ */
+function decodeToken(token) {
+    const [header, payload] = token.split(".");
+
+    if (typeof payload !== "string") {
+        throw new Error("Unable to decode token, payload not found.");
+    }
+
+    let decoded;
+
+    try {
+        decoded = base64UrlDecode(payload);
+    } catch (error) {
+        throw new Error("Unable to decode token, payload is not a valid Base64URL value.", {cause: error});
+    }
+
+    try {
+        return JSON.parse(decoded);
+    } catch (error) {
+        throw new Error("Unable to decode token, payload is not a valid JSON value.", {cause: error});
+    }
+}
+
+/**
+ * @param {string} input
+ */
+function base64UrlDecode(input) {
+    let output = input
+        .replaceAll("-", "+")
+        .replaceAll("_", "/");
+
+    switch (output.length % 4) {
+        case 0:
+            break;
+        case 2:
+            output += "==";
+            break;
+        case 3:
+            output += "=";
+            break;
+        default:
+            throw new Error("Input is not of the correct length.");
+    }
+
+    try {
+        return b64DecodeUnicode(output);
+    } catch (error) {
+        return atob(output);
+    }
+}
+
+/**
+ * @param {string} input
+ */
+function b64DecodeUnicode(input) {
+    return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => {
+        let code = p.charCodeAt(0).toString(16).toUpperCase();
+
+        if (code.length < 2) {
+            code = "0" + code;
+        }
+
+        return "%" + code;
+    }));
+}
+
+/**
+ * Check if the input is an object that can be operated on.
+ * @param {unknown} input
+ */
+function isObject(input) {
+    return typeof input === 'object' && input !== null;
+}
diff --git a/src/main/scala/akkahttp/oidc/OIDCKeycloak.scala b/src/main/scala/akkahttp/oidc/OIDCKeycloak.scala
index d070867f..3220e979 100644
--- a/src/main/scala/akkahttp/oidc/OIDCKeycloak.scala
+++ b/src/main/scala/akkahttp/oidc/OIDCKeycloak.scala
@@ -58,7 +58,7 @@ object OIDCKeycloak extends App with CORSHandler with JsonSupport {
 
   def runKeycloak() = {
     // Pin to same version as "keycloakVersion" in build.sbt
-    val keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:24.0.4")
+    val keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:26.0.1")
       // Keycloak config taken from:
       // https://github.com/keycloak/keycloak/blob/main/examples/js-console/example-realm.json
       .withRealmImportFile("keycloak_realm_config.json")
@@ -224,7 +224,14 @@ object OIDCKeycloak extends App with CORSHandler with JsonSupport {
         )
       }
 
-    val routes: Route = corsHandler(userRoutes) ~ getFromDocRoot
+    val getResource: Route =
+      pathPrefix("js") {
+        path(Remaining) { file =>
+          getFromFile(s"src/main/resources/js/$file")
+        }
+      }
+
+    val routes: Route = corsHandler(userRoutes) ~ getFromDocRoot ~ getResource
     val bindingFuture = Http().newServerAt("127.0.0.1", 6002).bind(routes)
 
     bindingFuture.onComplete {