diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index 37069eba9..a8b42b7a7 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -10413,6 +10413,11 @@ "easy-stack": "^1.0.1" } }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10554,6 +10559,22 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, + "keycloak-js": { + "version": "21.1.2", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-21.1.2.tgz", + "integrity": "sha512-+6r1BvmutWGJBtibo7bcFbHWIlA7XoXRCwcA4vopeJh59Nv2Js0ju2u+t8AYth+C6Cg7/BNfO3eCTbsl/dTBHw==", + "requires": { + "base64-js": "^1.5.1", + "js-sha256": "^0.9.0" + }, + "dependencies": { + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + } + } + }, "keyv": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index bf6de39a5..0220241eb 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -37,7 +37,8 @@ "vue-router": "^3.0.1", "vue-select": "^3.1.0", "vuejs-noty": "^0.1.3", - "vuex": "^3.0.1" + "vuex": "^3.0.1", + "keycloak-js": "21.1.2" }, "devDependencies": { "@vue/cli-plugin-babel": "^3.7.0", diff --git a/app/frontend/src/common/authenticate.js b/app/frontend/src/common/authenticate.js index eba2da481..954de609a 100644 --- a/app/frontend/src/common/authenticate.js +++ b/app/frontend/src/common/authenticate.js @@ -10,6 +10,7 @@ limitations under the License. */ import ApiService from '@/common/services/ApiService.js' +import Keycloak from 'keycloak-js'; import Vue from 'vue' export default { @@ -17,39 +18,40 @@ export default { /** * Returns a promise that resolves to an instance of Keycloak. */ + return new Promise((resolve, reject) => { if (!Vue.prototype.$keycloak) { - // Keycloak has not yet been loaded, get Keycloak configuration from the server. + ApiService.query('keycloak', {}) .then(response => { - /* - "A best practice is to load the JavaScript adapter directly from Keycloak Server as it will - automatically be updated when you upgrade the server. If you copy the adapter to your web - application instead, make sure you upgrade the adapter only after you have upgraded the server."; - source : https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter: - */ - const jsUrl = `${response.data['auth-server-url']}/js/keycloak.js` - // Inject the Keycloak javascript into the DOM. - const keyCloakScript = document.createElement('script') - keyCloakScript.onload = () => { - // Construct the Keycloak object and resolve the promise. - Vue.prototype.$keycloak = new Keycloak(response.data) - resolve(Vue.prototype.$keycloak) - } - keyCloakScript.onerror = (e) => { - // This is pretty bad - keycloak didn't load - this should never ever happen. - // There's not much we can do, so we set keycloak to a random empty object and resolve. - console.error(e) - Vue.prototype.$keycloak = {} - resolve(Vue.prototype.$keycloak) - } - keyCloakScript.async = true - keyCloakScript.setAttribute('src', jsUrl) - document.head.appendChild(keyCloakScript) + + const { + 'ssl-required': sslRequired, + resource, + realm, + 'public-client': publicClient, + 'confidential-port': confidentialPort, + clientId, + 'auth-server-url': authServerUrl + } = response.data; + + Vue.prototype.$keycloak = new Keycloak({ + url: authServerUrl, + realm, + clientId, + sslRequired, + resource, + publicClient, + confidentialPort, + }) + + resolve(Vue.prototype.$keycloak) + }) .catch(error => { + console.error(error) Vue.prototype.$keycloak = {} - reject(error) + resolve(Vue.prototype.$keycloak) }) } else { // Keycloak has already been loaded, so just resolve the object. @@ -86,13 +88,12 @@ export default { renewToken (instance, retries = 0) { const maxRetries = 2 - instance.updateToken(1800).success((refreshed) => { + instance.updateToken(1800).then((refreshed) => { if (refreshed) { this.setLocalToken(instance) } this.scheduleRenewal(instance) - }).error((e) => { - console.log(e) + }).catch((e) => { // The refresh token is expired or was rejected // we will retry after 60 sec (up to the count defined by maxRetries) if (retries > maxRetries) { @@ -112,27 +113,31 @@ export default { */ return new Promise((resolve, reject) => { this.getInstance() - .then((instance) => { + .then(async (instance) => { if (instance.authenticated && ApiService.hasAuthHeader() && !instance.isTokenExpired(0)) { // We've already authenticated, have a header, and we've not expired. resolve(instance) } else { - // Attempt to retrieve a stored token, this may avoid us having to refresh the page. - const token = localStorage.getItem('token') - const refreshToken = localStorage.getItem('refreshToken') - const idToken = localStorage.getItem('idToken') - instance.init({ - pkceMethod: 'S256', - onLoad: 'check-sso', - checkLoginIframe: true, - timeSkew: 10, // Allow for some deviation - token, - refreshToken, - idToken } - ).success((result) => { + + try { + // Attempt to retrieve a stored token, this may avoid us having to refresh the page. + const token = localStorage.getItem('token') + const refreshToken = localStorage.getItem('refreshToken') + const idToken = localStorage.getItem('idToken') + + const authed = await instance.init({ + pkceMethod: 'S256', + onLoad: 'check-sso', + timeSkew: 10, + checkLoginIframe: true, + token, + refreshToken, + idToken, + }) + if (instance.authenticated) { // We may have been authenticated, but the token could be expired. - instance.updateToken(60).success(() => { + instance.updateToken(60).then(() => { // Store the token to avoid future round trips, and wire up the API this.setLocalToken(instance) // We update the store reference only after wiring up the API. (Someone might be waiting @@ -141,7 +146,7 @@ export default { store.commit('SET_KEYCLOAK', instance) this.scheduleRenewal(instance) resolve(instance) - }).error(() => { + }).catch(() => { // The refresh token is expired or was rejected this.removeLocalToken() instance.clearToken() @@ -156,9 +161,11 @@ export default { store.commit('SET_KEYCLOAK', instance) resolve(instance) } - }).error((e) => { - reject(e) - }) + + } catch (error) { + console.error('Failed to initialize adapter:', error); + } + } }) .catch((error) => { diff --git a/app/frontend/src/common/components/Auth.vue b/app/frontend/src/common/components/Auth.vue index 8a54a3c86..12b2a7c20 100644 --- a/app/frontend/src/common/components/Auth.vue +++ b/app/frontend/src/common/components/Auth.vue @@ -38,8 +38,10 @@ export default { } }, keyCloakLogin () { - this.keycloak.init().success(() => { - this.keycloak.login({ idpHint: this.config.sso_idp_hint }).success((authenticated) => { + this.keycloak.init({ + checkLoginIframe: false + }).then(() => { + this.keycloak.login({ idpHint: this.config.sso_idp_hint }).then((authenticated) => { if (authenticated) { ApiService.authHeader('JWT', this.keycloak.token) if (window.localStorage) { @@ -48,7 +50,8 @@ export default { localStorage.setItem('idToken', this.keycloak.idToken) } } - }).error((e) => { + }).catch((e) => { + console.error("keyCloakLogin: ", e) this.$store.commit(SET_ERROR, { error: 'Cannot contact SSO provider' }) }) })