diff --git a/assembly/assembly-root-war/src/main/webapp/_app/keycloackLoader.js b/assembly/assembly-root-war/src/main/webapp/_app/keycloackLoader.js new file mode 100644 index 00000000000..ce0e07cc8e4 --- /dev/null +++ b/assembly/assembly-root-war/src/main/webapp/_app/keycloackLoader.js @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +export class KeycloakLoader { + /** + * Load keycloak settings + */ + loadKeycloakSettings() { + const msg = "Cannot load keycloak settings. This is normal for single-user mode."; + + return new Promise((resolve, reject) => { + try { + if (window.parent && window.parent['_keycloak']) { + window['_keycloak'] = window.parent['_keycloak']; + resolve(window['_keycloak']); + return; + } + } catch (e) { + // parent frame has different origin, so access to parent frame is forbidden + console.error(msg, e); + } + + try { + const request = new XMLHttpRequest(); + + request.onerror = request.onabort = function() { + reject(new Error(msg)); + }; + + request.onload = () => { + if (request.status == 200) { + resolve(this.injectKeycloakScript(JSON.parse(request.responseText))); + } else { + reject(new Error(msg)); + } + }; + + const url = "/api/keycloak/settings"; + request.open("GET", url, true); + request.send(); + } catch (e) { + reject(new Error(msg + e.message)); + } + }); + } + + /** + * Injects keycloak javascript + */ + injectKeycloakScript(keycloakSettings) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.language = 'javascript'; + script.async = true; + script.src = keycloakSettings['che.keycloak.js_adapter_url']; + + script.onload = () => { + resolve(this.initKeycloak(keycloakSettings)); + }; + + script.onerror = script.onabort = () => { + reject(new Error('cannot load ' + script.src)); + }; + + document.head.appendChild(script); + }); + } + + /** + * Initialize keycloak + */ + initKeycloak(keycloakSettings) { + return new Promise((resolve, reject) => { + + function keycloakConfig() { + const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider']; + if (!theOidcProvider) { + return { + url: keycloakSettings['che.keycloak.auth_server_url'], + realm: keycloakSettings['che.keycloak.realm'], + clientId: keycloakSettings['che.keycloak.client_id'] + }; + } else { + return { + oidcProvider: theOidcProvider, + clientId: keycloakSettings['che.keycloak.client_id'] + }; + } + } + const keycloak = Keycloak(keycloakConfig()); + + window['_keycloak'] = keycloak; + + var useNonce; + if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { + useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; + } + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); + keycloak + .init({ + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: useNonce, + scope: 'email profile', + redirectUri: keycloakSettings['che.keycloak.redirect_url.ide'] + }) + .success(() => { + resolve(keycloak); + }) + .error(() => { + reject(new Error('[Keycloak] Failed to initialize Keycloak')); + }); + }); + } + +} diff --git a/assembly/assembly-root-war/src/main/webapp/_app/loader.html b/assembly/assembly-root-war/src/main/webapp/_app/loader.html index f3291aeaf67..53d78183f79 100644 --- a/assembly/assembly-root-war/src/main/webapp/_app/loader.html +++ b/assembly/assembly-root-war/src/main/webapp/_app/loader.html @@ -18,8 +18,8 @@ Workspace token loader - - + + diff --git a/assembly/assembly-root-war/src/main/webapp/_app/loader.js b/assembly/assembly-root-war/src/main/webapp/_app/loader.js index d1045a7c727..ca773caa4df 100644 --- a/assembly/assembly-root-war/src/main/webapp/_app/loader.js +++ b/assembly/assembly-root-war/src/main/webapp/_app/loader.js @@ -9,120 +9,8 @@ * Contributors: * Red Hat, Inc. - initial API and implementation */ -class KeycloakLoader { - /** - * Load keycloak settings - */ - loadKeycloakSettings() { - const msg = "Cannot load keycloak settings. This is normal for single-user mode."; - - return new Promise((resolve, reject) => { - try { - if (window.parent && window.parent['_keycloak']) { - window['_keycloak'] = window.parent['_keycloak']; - resolve(window['_keycloak']); - return; - } - } catch (e) { - // parent frame has different origin, so access to parent frame is forbidden - console.error(msg, e); - } - - try { - const request = new XMLHttpRequest(); - - request.onerror = request.onabort = function() { - reject(new Error(msg)); - }; - - request.onload = () => { - if (request.status == 200) { - resolve(this.injectKeycloakScript(JSON.parse(request.responseText))); - } else { - reject(new Error(msg)); - } - }; - - const url = "/api/keycloak/settings"; - request.open("GET", url, true); - request.send(); - } catch (e) { - reject(new Error(msg + e.message)); - } - }); - } - - /** - * Injects keycloak javascript - */ - injectKeycloakScript(keycloakSettings) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.language = 'javascript'; - script.async = true; - script.src = keycloakSettings['che.keycloak.js_adapter_url']; - script.onload = () => { - resolve(this.initKeycloak(keycloakSettings)); - }; - - script.onerror = script.onabort = () => { - reject(new Error('cannot load ' + script.src)); - }; - - document.head.appendChild(script); - }); - } - - /** - * Initialize keycloak - */ - initKeycloak(keycloakSettings) { - return new Promise((resolve, reject) => { - - function keycloakConfig() { - const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider']; - if (!theOidcProvider) { - return { - url: keycloakSettings['che.keycloak.auth_server_url'], - realm: keycloakSettings['che.keycloak.realm'], - clientId: keycloakSettings['che.keycloak.client_id'] - }; - } else { - return { - oidcProvider: theOidcProvider, - clientId: keycloakSettings['che.keycloak.client_id'] - }; - } - } - const keycloak = Keycloak(keycloakConfig()); - - window['_keycloak'] = keycloak; - - var useNonce; - if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { - useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; - } - window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); - keycloak - .init({ - onLoad: 'login-required', - checkLoginIframe: false, - useNonce: useNonce, - scope: 'email profile', - redirectUri: keycloakSettings['che.keycloak.redirect_url.ide'] - }) - .success(() => { - resolve(keycloak); - }) - .error(() => { - reject(new Error('[Keycloak] Failed to initialize Keycloak')); - }); - }); - } - -} +import { KeycloakLoader } from './keycloackLoader.js'; class Loader { diff --git a/assembly/assembly-root-war/src/main/webapp/_app/oauth.html b/assembly/assembly-root-war/src/main/webapp/_app/oauth.html new file mode 100644 index 00000000000..615ed6698cc --- /dev/null +++ b/assembly/assembly-root-war/src/main/webapp/_app/oauth.html @@ -0,0 +1,29 @@ + + + + + + + Authentication + + + + + +
+ + + + diff --git a/assembly/assembly-root-war/src/main/webapp/_app/oauthLoader.js b/assembly/assembly-root-war/src/main/webapp/_app/oauthLoader.js new file mode 100644 index 00000000000..85af5c72f8e --- /dev/null +++ b/assembly/assembly-root-war/src/main/webapp/_app/oauthLoader.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { KeycloakLoader } from './keycloackLoader.js'; + +/** + * Displays error message + * @param {string} message an error message to show + */ +function error(message) { + const container = document.getElementById("error-console"); + + const element = document.createElement("pre"); + element.innerHTML = message; + element.style.color = 'red'; + container.appendChild(element); + return element; +} + +/** + * Returns an array of query parameter values if it is present + * @param {string} name of the query parameter + */ +function getQueryParams(name) { + const queryString = window.location.search; + return new URLSearchParams(queryString).getAll(name); +} + +/** + * Fetches userId + */ +function asyncGetUserId() { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + request.open("GET", '/api/user'); + setAuthorizationHeader(request).then((xhr) => { + xhr.send(); + xhr.onreadystatechange = () => { + if (xhr.readyState !== 4) { + return; + } + if (xhr.status !== 200) { + const errorMessage = 'Failed to get the workspace: "' + this.getRequestErrorMessage(xhr) + '"'; + reject(new Error(errorMessage)); + return; + } + resolve(JSON.parse(xhr.responseText).id); + }; + }); + }); +} + +/** + * Sets authorization header for a request + * @param {XMLHttpRequest} xhr + */ +function setAuthorizationHeader(xhr) { + return new Promise((resolve, reject) => { + if (window._keycloak && window._keycloak.token) { + window._keycloak.updateToken(5).success(() => { + xhr.setRequestHeader('Authorization', 'Bearer ' + window._keycloak.token); + resolve(xhr); + }).error(() => { + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); + window._keycloak.login(); + reject(new Error('Failed to refresh token')); + }); + } + + resolve(xhr); + }); +} + +function postMessage(message) { + if (window.opener) { + window.opener.postMessage(message, '*'); + } +} + +/** + * Fetches workspace details by ID + * @param {string} workspaceId a workspace id + */ +function getWorkspace(workspaceId) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + request.open("GET", '/api/workspace/' + workspaceId); + setAuthorizationHeader(request).then((xhr) => { + xhr.send(); + xhr.onreadystatechange = () => { + if (xhr.readyState !== 4) { + return; + } + if (xhr.status !== 200) { + const errorMessage = 'Failed to get the workspace: "' + this.getRequestErrorMessage(xhr) + '"'; + reject(new Error(errorMessage)); + return; + } + resolve(undefined); + }; + }); + }); +} + + +function parseToken (token) { + return JSON.parse(atob(token.split('.')[1])); +} + +(async () => { + let token; + try { + const keycloak = await new KeycloakLoader().loadKeycloakSettings(); + token = keycloak ? keycloak.token : undefined; + } catch (e) { + console.log(e.message); + } + try { + if (token) { + await new Promise((resolve, reject) => { + window.addEventListener('message', async data => { + if (data.data.startsWith('token:')) { + const machineToken = parseToken(data.data.substring(6, data.data.length)); + const userToken = parseToken(token); + if (machineToken.uid === userToken.sub) { + try { + await getWorkspace(machineToken.wsid); + } catch (e) { + reject(e); + } + } else { + reject(new Error('Machine and user token mismatch')); + } + resolve(undefined); + } + }); + postMessage('status:ready-to-receive-messages'); + }); + } + const status = getQueryParams('status')[0]; { + if (status && status === 'ready') { + postMessage('token:' + (token ? token : '')); + return; + } + } + const oauthProvider = getQueryParams('oauth_provider')[0]; + if (!oauthProvider) { + postMessage('token:' + (token ? token : '')); + return; + } + const currentUrl = window.location.href; + const cheUrl = currentUrl.substring(0, currentUrl.indexOf('/_app')); + const apiUrl = cheUrl + '/api'; + const redirectUrl = currentUrl.substring(0, currentUrl.indexOf('?')) + '?status=ready'; + let url = `${apiUrl}/oauth/authenticate?oauth_provider=${oauthProvider}&userId=${await asyncGetUserId()}`; + const scope = getQueryParams('scope'); { + for (const s of scope) { + url += `&scope=${s}`; + } + } + url += `&redirect_after_login=${redirectUrl}`; + if (token) { + url += `&token=${token}`; + } + window.location.replace(url); + } catch (errorMessage) { + error(errorMessage); + console.error(errorMessage); + } +}) +(); diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties index cc5675e916e..a976e6e0802 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties @@ -161,4 +161,4 @@ che.keycloak.username_claim=NULL # If set to "embedded", then the service work as a wrapper to Che's OAuthAuthenticator ( as in Single User mode). # If set to "delegated", then the service will use Keycloak IdentityProvider mechanism. # Runtime Exception wii be thrown, in case if this property is not set properly. -che.oauth.service_mode=embedded +che.oauth.service_mode=delegated diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java index 1d61f70cdf7..4a3826dbe28 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java @@ -82,9 +82,6 @@ protected void configure() { machineAuthenticatedResources .addBinding() .toInstance(new MachineAuthenticatedResource("/activity", "active")); - machineAuthenticatedResources - .addBinding() - .toInstance(new MachineAuthenticatedResource("oauth", "token")); machineAuthenticatedResources .addBinding()