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 dfe8a6429e..8abe5cf8e0 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 @@ -152,6 +152,12 @@ che.keycloak.js_adapter_url=NULL # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig che.keycloak.oidc_provider=NULL +# Set to true when using an alternate OIDC provider that +# only supports fixed redirect Urls +#. +# This property is ignored when `che.keycloak.oidc_provider` is NULL +che.keycloak.use_fixed_redirect_urls=false + # Username claim to be used as user display name # when parsing JWT token # if not defined the fallback value is 'preferred_username' diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js index ae093a78e7..9ae4cf3866 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js @@ -36,19 +36,27 @@ interval: 5 }; + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + if ((scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && scripts[i].src.indexOf('version=') !== -1) { + kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0]; + } + } + var useNonce = true; kc.init = function (initOptions) { kc.authenticated = false; callbackStorage = createCallbackStorage(); + var adapters = ['default', 'cordova', 'cordova-native']; - if (initOptions && initOptions.adapter === 'cordova') { - adapter = loadAdapter('cordova'); - } else if (initOptions && initOptions.adapter === 'default') { - adapter = loadAdapter(); + if (initOptions && adapters.indexOf(initOptions.adapter) > -1) { + adapter = loadAdapter(initOptions.adapter); + } else if (initOptions && typeof initOptions.adapter === "object") { + adapter = initOptions.adapter; } else { - if (window.Cordova) { + if (window.Cordova || window.cordova) { adapter = loadAdapter('cordova'); } else { adapter = loadAdapter(); @@ -100,6 +108,14 @@ if (initOptions.timeSkew != null) { kc.timeSkew = initOptions.timeSkew; } + + if(initOptions.redirectUri) { + kc.redirectUri = initOptions.redirectUri; + } + + if(initOptions.scope) { + kc.scope = initOptions.scope; + } } if (!kc.responseMode) { @@ -129,8 +145,8 @@ } kc.login(options).success(function () { initPromise.setSuccess(); - }).error(function () { - initPromise.setError(); + }).error(function (errorData) { + initPromise.setError(errorData); }); } @@ -161,10 +177,15 @@ var callback = parseCallback(window.location.href); if (callback) { - setupCheckLoginIframe(); window.history.replaceState({}, null, callback.newUrl); - processCallback(callback, initPromise); - return; + } + + if (callback && callback.valid) { + return setupCheckLoginIframe().success(function() { + processCallback(callback, initPromise); + }).error(function (e) { + initPromise.setError(); + }); } else if (initOptions) { if (initOptions.token && initOptions.refreshToken) { setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); @@ -223,7 +244,7 @@ var callbackState = { state: state, nonce: nonce, - redirectUri: encodeURIComponent(redirectUri), + redirectUri: encodeURIComponent(redirectUri) } if (options && options.prompt) { @@ -239,7 +260,22 @@ baseUrl = kc.endpoints.authorize(); } - var scope = (options && options.scope) ? "openid " + options.scope : "openid"; + var providedScope; + if (options && options.scope) { + providedScope = options.scope; + } else if (kc.scope) { + providedScope = kc.scope; + } + var scope; + if (providedScope) { + if (providedScope.indexOf("openid") != -1) { + scope = providedScope; + } else { + scope = "openid " + providedScope; + } + } else { + scope = "openid"; + } var url = baseUrl + '?client_id=' + encodeURIComponent(kc.clientId) @@ -271,6 +307,10 @@ if (options && options.locale) { url += '&ui_locales=' + encodeURIComponent(options.locale); } + + if (options && options.kcLocale) { + url += '&kc_locale=' + encodeURIComponent(options.kcLocale); + } return url; } @@ -455,6 +495,10 @@ } else { console.warn('[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); @@ -557,8 +601,38 @@ var tokenResponse = JSON.parse(req.responseText); authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard'); } else { - kc.onAuthError && kc.onAuthError(); - promise && promise.setError(); + var errorData = {}; + var json; + try { + json = JSON.parse(req.response); + } catch(err) { + } + + if (json && + json.error) { + errorData['error'] = json.error; + if (json.error_description) { + errorData['error_description'] = json.error_description; + } + if (json.error_uri) { + errorData['error_uri'] = json.error_uri; + } + } else { + errorData['error'] = 'invalid_request'; + var description = { + status: req.status, + response: req.responseText, + }; + try { + var authHeader = req.getResponseHeader('WWW-Authenticate'); + if (authHeader) { + description['www_authenticate_header'] = authHeader; + } + } catch(err) {} + errorData['error_description'] = JSON.stringify(description); + } + kc.onAuthError && kc.onAuthError(errorData); + promise && promise.setError(errorData); } } }; @@ -611,7 +685,11 @@ return getRealmUrl() + '/protocol/openid-connect/logout'; }, checkSessionIframe: function() { - return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; + var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; + if (kc.iframeVersion) { + src = src + '?version=' + kc.iframeVersion; + } + return src; }, register: function() { return getRealmUrl() + '/protocol/openid-connect/registrations'; @@ -851,23 +929,137 @@ } function parseCallback(url) { - var oauth = new CallbackParser(url, kc.responseMode).parseUri(); + var oauth = parseCallbackUrl(url); + if (!oauth) { + return; + } + var oauthState = callbackStorage.get(oauth.state); - if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) { + if (oauthState) { + oauth.valid = true; oauth.redirectUri = oauthState.redirectUri; oauth.storedNonce = oauthState.nonce; oauth.prompt = oauthState.prompt; + } - if (oauth.fragment) { - oauth.newUrl += '#' + oauth.fragment; + return oauth; + } + + function parseCallbackUrl(url) { + var supportedParams; + switch (kc.flow) { + case 'standard': + supportedParams = ['code', 'state', 'session_state']; + break; + case 'implicit': + supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in']; + break; + case 'hybrid': + supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state']; + 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; + } + } - return oauth; + 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 t = p[i].split('='); + if (supportedParams.indexOf(t[0]) !== -1) { + result.oauthParams[t[0]] = t[1]; + } else { + if (result.paramsString !== '') { + result.paramsString += '&'; + } + result.paramsString += p[i]; + } + } + return result; + } + function createPromise() { + if (typeof Promise === "function") { + return createNativePromise(); + } else { + return createLegacyPromise(); + } + } + + function createNativePromise() { + // 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.success = true; + p.resolve(result); + }, + + setError: function(result) { + p.success = false; + p.reject(result); + } + }; + p.promise = new Promise(function(resolve, reject) { + p.resolve = resolve; + p.reject = reject; + }); + p.promise.success = function(callback) { + p.promise.then(callback); + return p.promise; + } + p.promise.error = function(callback) { + p.promise.catch(callback); + return p.promise; + } + return p; + } + + function createLegacyPromise() { var p = { setSuccess: function(result) { p.success = true; @@ -924,7 +1116,7 @@ loginIframe.iframe = iframe; iframe.onload = function() { - var authUrl = kc.authorize(); + var authUrl = kc.endpoints.authorize(); if (authUrl.charAt(0) === '/') { loginIframe.iframeOrigin = getOrigin(); } else { @@ -935,8 +1127,9 @@ setTimeout(check, loginIframe.interval * 1000); } - var src = kc.checkSessionIframe(); + var src = kc.endpoints.checkSessionIframe(); iframe.setAttribute('src', src ); + iframe.setAttribute('title', 'keycloak-session-iframe' ); iframe.style.display = 'none'; document.body.appendChild(iframe); @@ -1033,12 +1226,7 @@ } else if (kc.redirectUri) { return kc.redirectUri; } else { - var redirectUri = location.href; - if (location.hash && encodeHash) { - redirectUri = redirectUri.substring(0, location.href.indexOf('#')); - redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); - } - return redirectUri; + return location.href; } } }; @@ -1046,26 +1234,62 @@ 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); + }; return { login: function(options) { var promise = createPromise(); - var o = 'location=no'; - if (options && options.prompt == 'none') { - o += ',hidden=yes'; - } - + var cordovaOptions = createCordovaOptions(options); var loginUrl = kc.createLoginUrl(options); - var ref = window.open(loginUrl, '_blank', o); - + 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('http://localhost') == 0) { var callback = parseCallback(event.url); processCallback(callback, promise); - ref.close(); + closeBrowser(); completed = true; } }); @@ -1075,23 +1299,31 @@ if (event.url.indexOf('http://localhost') == 0) { var callback = parseCallback(event.url); processCallback(callback, promise); - ref.close(); + closeBrowser(); completed = true; } else { promise.setError(); - ref.close(); + 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 = window.open(logoutUrl, '_blank', 'location=no,hidden=yes'); + var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes'); var error; @@ -1124,7 +1356,8 @@ register : function() { var registerUrl = kc.createRegisterUrl(); - var ref = window.open(registerUrl, '_blank', 'location=no'); + var cordovaOptions = createCordovaOptions(options); + var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions); ref.addEventListener('loadstart', function(event) { if (event.url.indexOf('http://localhost') == 0) { ref.close(); @@ -1135,7 +1368,7 @@ accountManagement : function() { var accountUrl = kc.createAccountUrl(); if (typeof accountUrl !== 'undefined') { - var ref = window.open(accountUrl, '_blank', 'location=no'); + var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no'); ref.addEventListener('loadstart', function(event) { if (event.url.indexOf('http://localhost') == 0) { ref.close(); @@ -1152,6 +1385,75 @@ } } + if (type == 'cordova-native') { + loginIframe.enable = false; + + return { + login: function(options) { + var promise = createPromise(); + var loginUrl = 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 : function(options) { + var promise = createPromise(); + var registerUrl = 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; } @@ -1273,99 +1575,6 @@ return new CookieStorage(); } - - var CallbackParser = function(uriToParse, responseMode) { - if (!(this instanceof CallbackParser)) { - return new CallbackParser(uriToParse, responseMode); - } - var parser = this; - - var initialParse = function() { - var baseUri = null; - var queryString = null; - var fragmentString = null; - - var questionMarkIndex = uriToParse.indexOf("?"); - var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1); - if (questionMarkIndex == -1 && fragmentIndex == -1) { - baseUri = uriToParse; - } else if (questionMarkIndex != -1) { - baseUri = uriToParse.substring(0, questionMarkIndex); - queryString = uriToParse.substring(questionMarkIndex + 1); - if (fragmentIndex != -1) { - fragmentIndex = queryString.indexOf("#"); - fragmentString = queryString.substring(fragmentIndex + 1); - queryString = queryString.substring(0, fragmentIndex); - } - } else { - baseUri = uriToParse.substring(0, fragmentIndex); - fragmentString = uriToParse.substring(fragmentIndex + 1); - } - - return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString }; - } - - var parseParams = function(paramString) { - var result = {}; - var params = paramString.split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - var paramName = decodeURIComponent(p[0]); - var paramValue = decodeURIComponent(p[1]); - result[paramName] = paramValue; - } - return result; - } - - var handleQueryParam = function(paramName, paramValue, oauth) { - var supportedOAuthParams = [ 'code', 'state', 'error', 'error_description' ]; - - for (var i = 0 ; i< supportedOAuthParams.length ; i++) { - if (paramName === supportedOAuthParams[i]) { - oauth[paramName] = paramValue; - return true; - } - } - return false; - } - - - parser.parseUri = function() { - var parsedUri = initialParse(); - - var queryParams = {}; - if (parsedUri.queryString) { - queryParams = parseParams(parsedUri.queryString); - } - - var oauth = { newUrl: parsedUri.baseUri }; - for (var param in queryParams) { - switch (param) { - case 'redirect_fragment': - oauth.fragment = queryParams[param]; - break; - default: - if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) { - oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]); - } - break; - } - } - - if (responseMode === 'fragment') { - var fragmentParams = {}; - if (parsedUri.fragmentString) { - fragmentParams = parseParams(parsedUri.fragmentString); - } - for (var param in fragmentParams) { - oauth[param] = fragmentParams[param]; - } - } - - return oauth; - } - } - } if ( typeof module === "object" && module && typeof module.exports === "object" ) { @@ -1377,4 +1586,4 @@ define( "keycloak", [], function () { return Keycloak; } ); } } -})( window ); \ No newline at end of file +})( window ); diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallback.js b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallback.js new file mode 100644 index 0000000000..001e333281 --- /dev/null +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallback.js @@ -0,0 +1,26 @@ +/* + * 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 + */ + +function redirectToInitialPage(ide) { + var redirectUri = window.sessionStorage.getItem('oidc' + (ide ? 'Ide' : 'Dashboard') + 'RedirectUrl'); + var fragmentIndex = redirectUri.indexOf('#'); + if (location.hash) { + var keycloakParameters; + if (fragmentIndex == -1) { + keycloakParameters = location.hash; + } else { + keycloakParameters = '&' + location.hash.substring(1); + } + redirectUri += keycloakParameters; + } + window.location = redirectUri; +} diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackDashboard.html b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackDashboard.html new file mode 100644 index 0000000000..6f03c5f223 --- /dev/null +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackDashboard.html @@ -0,0 +1,20 @@ + + + + oidcCallback + + + + diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackIde.html b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackIde.html new file mode 100644 index 0000000000..e0a25adafe --- /dev/null +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/oidcCallbackIde.html @@ -0,0 +1,20 @@ + + + + oidcCallback + + + + diff --git a/dashboard/src/app/index.module.ts b/dashboard/src/app/index.module.ts index 51b08207fd..15c95a4467 100755 --- a/dashboard/src/app/index.module.ts +++ b/dashboard/src/app/index.module.ts @@ -80,11 +80,17 @@ function keycloakLoad(keycloakSettings: any) { document.head.appendChild(script); }); } -function keycloakInit(keycloakConfig: any, theUseNonce: boolean) { + +function keycloakInit(keycloakConfig: any, initOptions: any) { return new Promise((resolve: IResolveFn, reject: IRejectFn) => { const keycloak = Keycloak(keycloakConfig); + window.sessionStorage.setItem('oidcDashboardRedirectUrl', location.href); keycloak.init({ - onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: initOptions['useNonce'], + scope: 'email profile', + redirectUri: initOptions['redirectUrl']; }).success(() => { resolve(keycloak); }).error((error: any) => { @@ -100,6 +106,7 @@ function setAuthorizationHeader(xhr: XMLHttpRequest, keycloak: any): Promise { console.log('Failed to refresh token'); + window.sessionStorage.setItem('oidcDashboardRedirectUrl', location.href); keycloak.login(); reject('Authorization is needed.'); }); @@ -150,11 +157,15 @@ angular.element(document).ready(() => { // load Keycloak return keycloakLoad(keycloakSettings).then(() => { // init Keycloak - var useNonce: boolean; + var theUseNonce: boolean; if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { - useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; + theUseNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; } - return keycloakInit(keycloakAuth.config, useNonce); + var initOptions = { + useNonce: theUseNonce, + redirectUrl: keycloakSettings['che.keycloak.redirect_url.dashboard'] + } + return keycloakInit(keycloakAuth.config, initOptions); }).then((keycloak: any) => { keycloakAuth.isPresent = true; keycloakAuth.keycloak = keycloak; diff --git a/dashboard/src/app/workspaces/create-workspace/project-source-selector/add-import-project/import-github-project/import-github-project.controller.ts b/dashboard/src/app/workspaces/create-workspace/project-source-selector/add-import-project/import-github-project/import-github-project.controller.ts index 3d745400ad..6b6b83fcee 100644 --- a/dashboard/src/app/workspaces/create-workspace/project-source-selector/add-import-project/import-github-project/import-github-project.controller.ts +++ b/dashboard/src/app/workspaces/create-workspace/project-source-selector/add-import-project/import-github-project/import-github-project.controller.ts @@ -205,6 +205,15 @@ export class ImportGithubProjectController { this.importGithubProjectService.onRepositorySelected(this.selectedRepositories); } + private storeRedirectUri(encodeHash) { + var redirectUri = location.href; + if (location.hash && encodeHash) { + redirectUri = redirectUri.substring(0, location.href.indexOf('#')); + redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); + } + window.sessionStorage.setItem('oidcIdeRedirectUrl', redirectUri); + } + /** * Shows authentication popup window. */ @@ -226,6 +235,7 @@ export class ImportGithubProjectController { let token = '&token=' + this.keycloakAuth.keycloak.token; this.openGithubPopup(token); }).error(() => { + window.sessionStorage.setItem('oidcDashboardRedirectUrl', location.href); this.keycloakAuth.keycloak.login(); }); } else { diff --git a/dashboard/src/components/api/che-keycloak.factory.ts b/dashboard/src/components/api/che-keycloak.factory.ts index d1621f7295..68f190b2d0 100644 --- a/dashboard/src/components/api/che-keycloak.factory.ts +++ b/dashboard/src/components/api/che-keycloak.factory.ts @@ -11,6 +11,7 @@ */ 'use strict'; + export type keycloakUserInfo = { email: string; family_name: string; @@ -67,7 +68,8 @@ export class CheKeycloak { } logout(): void { - this.keycloak.logout(); + window.sessionStorage.setItem('oidcDashboardRedirectUrl', location.href); + this.keycloak.logout({}); } } diff --git a/dashboard/src/components/interceptor/keycloak-token-interceptor.ts b/dashboard/src/components/interceptor/keycloak-token-interceptor.ts index 55f4a663f7..395be5f0d9 100644 --- a/dashboard/src/components/interceptor/keycloak-token-interceptor.ts +++ b/dashboard/src/components/interceptor/keycloak-token-interceptor.ts @@ -53,6 +53,7 @@ export class KeycloakTokenInterceptor extends HttpInterceptorBase { return config; } + if (this.keycloak && this.keycloak.token) { let deferred = this.$q.defer(); this.keycloak.updateToken(5).success(() => { @@ -62,6 +63,7 @@ export class KeycloakTokenInterceptor extends HttpInterceptorBase { }).error(() => { this.$log.log('token refresh failed :' + config.url); deferred.reject('Failed to refresh token'); + window.sessionStorage.setItem('oidcDashboardRedirectUrl', location.href); this.keycloak.login(); }); return deferred.promise; diff --git a/dockerfiles/init/manifests/che.env b/dockerfiles/init/manifests/che.env index 9af25b7e4d..f597d7d2c8 100644 --- a/dockerfiles/init/manifests/che.env +++ b/dockerfiles/init/manifests/che.env @@ -613,6 +613,11 @@ CHE_SINGLE_PORT=false # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig #CHE_KEYCLOAK_OIDC__PROVIDER=NULL +# Set to true when using an alternate OIDC provider that +# only supports fixed redirect Urls +#. +# This property is ignored when `CHE_KEYCLOAK_OIDC__PROVIDER` is NULL +CHE_KEYCLOAK_USE__FIXED__REDIRECT__URLS=false ######################################################################################## ##### ##### diff --git a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html index c1d535a826..3e41d5f9e4 100644 --- a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html +++ b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html @@ -139,8 +139,15 @@ useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; } + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); keycloak - .init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce}) + .init({ + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: useNonce, + scope: 'email profile', + redirectUri: keycloakSettings['che.keycloak.redirect_url.ide'] + }) .success(function(authenticated) { Loader.startLoading(); }) diff --git a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/loader.js b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/loader.js index f4557015d3..9122ea9088 100644 --- a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/loader.js +++ b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/loader.js @@ -69,6 +69,7 @@ class KeycloakLoader { */ initKeycloak(keycloakSettings) { return new Promise((resolve, reject) => { + function keycloakConfig() { const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider']; if (!theOidcProvider) { @@ -92,8 +93,15 @@ class KeycloakLoader { 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 }) + .init({ + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: useNonce, + scope: 'email profile', + redirectUri: keycloakSettings['che.keycloak.redirect_url.ide'] + }) .success(() => { resolve(keycloak); }) @@ -202,6 +210,7 @@ class Loader { 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')); }); diff --git a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java index 240c659253..bb1ec0c9a6 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java +++ b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java @@ -44,7 +44,8 @@ public final class Keycloak extends JavaScriptObject { String theRealm, String theClientId, String theOidcProvider, - boolean theUseNonce) /*-{ + boolean theUseNonce, + String redirectUrl) /*-{ return new Promise(function (resolve, reject) { try { console.log('[Keycloak] Initializing'); @@ -63,7 +64,14 @@ public final class Keycloak extends JavaScriptObject { } var keycloak = $wnd.Keycloak(config); $wnd['_keycloak'] = keycloak; - keycloak.init({onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce}) + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); + keycloak.init({ + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: theUseNonce, + scope: 'email profile', + redirectUri: redirectUrl + }) .success(function (authenticated) { resolve(keycloak); }) @@ -90,11 +98,13 @@ public final class Keycloak extends JavaScriptObject { .error(function () { console.log('[Keycloak] Failed updating Keycloak token'); reject(); + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); theKeycloak.login(); }); } catch (ex) { console.log('[Keycloak] Failed updating Keycloak token with exception: ', ex); reject(); + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); theKeycloak.login(); } }); diff --git a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java index 314dcd861e..42500b7d58 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java +++ b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java @@ -13,6 +13,7 @@ package org.eclipse.che.multiuser.keycloak.ide; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_IDE; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING; @@ -86,7 +87,8 @@ public class KeycloakProvider { settings.get(REALM_SETTING), settings.get(CLIENT_ID_SETTING), settings.get(OIDC_PROVIDER_SETTING), - Boolean.valueOf(settings.get(USE_NONCE_SETTING)).booleanValue())); + Boolean.valueOf(settings.get(USE_NONCE_SETTING)).booleanValue(), + settings.get(FIXED_REDIRECT_URL_FOR_IDE))); Log.debug(getClass(), "Keycloak init complete: ", this); } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java index 39946440aa..67b2da5b38 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java @@ -50,12 +50,9 @@ public class KeycloakConfigurationService extends Service { return keycloakSettings.get(); } - @GET - @Path("/OIDCKeycloak.js") - @Produces("text/javascript") - public String javascriptAdapter() throws IOException { + private String getKeycloakResource(String fileName) throws IOException { URL resource = - Thread.currentThread().getContextClassLoader().getResource("keycloak/OIDCKeycloak.js"); + Thread.currentThread().getContextClassLoader().getResource("keycloak/" + fileName); if (resource != null) { URLConnection conn = resource.openConnection(); try (InputStream is = conn.getInputStream(); @@ -70,4 +67,32 @@ public class KeycloakConfigurationService extends Service { } return ""; } + + @GET + @Path("/OIDCKeycloak.js") + @Produces("text/javascript") + public String javascriptAdapter() throws IOException { + return getKeycloakResource("OIDCKeycloak.js"); + } + + @GET + @Path("/oidcCallback.js") + @Produces("text/javascript") + public String callbackScript() throws IOException { + return getKeycloakResource("oidcCallback.js"); + } + + @GET + @Path("/oidcCallbackIde.html") + @Produces("text/html") + public String ideCallback() throws IOException { + return getKeycloakResource("oidcCallbackIde.html"); + } + + @GET + @Path("/oidcCallbackDashboard.html") + @Produces("text/html") + public String dashboardCallback() throws IOException { + return getKeycloakResource("oidcCallbackDashboard.html"); + } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java index b406c10013..fecf7833a0 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwt; import java.io.IOException; import java.security.Principal; +import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import javax.servlet.FilterChain; @@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpSession; import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.user.User; import org.eclipse.che.commons.auth.token.RequestTokenExtractor; @@ -49,10 +51,12 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt private final RequestTokenExtractor tokenExtractor; private final PermissionChecker permissionChecker; private final KeycloakSettings keycloakSettings; + private final KeycloakProfileRetriever keycloakProfileRetriever; @Inject public KeycloakEnvironmentInitalizationFilter( KeycloakUserManager userManager, + KeycloakProfileRetriever keycloakProfileRetriever, RequestTokenExtractor tokenExtractor, PermissionChecker permissionChecker, KeycloakSettings settings) { @@ -60,6 +64,7 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt this.tokenExtractor = tokenExtractor; this.permissionChecker = permissionChecker; this.keycloakSettings = settings; + this.keycloakProfileRetriever = keycloakProfileRetriever; } @Override @@ -91,14 +96,36 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt username = claims.getIssuer() + ":" + claims.getSubject(); } String email = claims.get("email", String.class); + String id = claims.getSubject(); + if (isNullOrEmpty(email)) { - sendError( - response, - 400, - "Unable to authenticate user because email address is not set in keycloak profile"); - return; + boolean userNotFound = false; + try { + userManager.getById(id); + } catch (NotFoundException e) { + userNotFound = true; + } + if (userNotFound) { + try { + EnvironmentContext.getCurrent() + .setSubject(new SubjectImpl(username, id, token, true)); + Map profileAttributes = + keycloakProfileRetriever.retrieveKeycloakAttributes(); + email = profileAttributes.get("email"); + if (email == null) { + sendError( + response, + 400, + "Unable to authenticate user because email address is not set in keycloak profile"); + return; + } + } finally { + EnvironmentContext.reset(); + } + } } - User user = userManager.getOrCreateUser(claims.getSubject(), email, username); + + User user = userManager.getOrCreateUser(id, email, username); subject = new AuthorizedSubject( new SubjectImpl(user.getName(), user.getId(), token, false), permissionChecker); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakHttpJsonRequestFactory.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakHttpJsonRequestFactory.java index 574a4ffa22..93f9c702f6 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakHttpJsonRequestFactory.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakHttpJsonRequestFactory.java @@ -29,13 +29,13 @@ public class KeycloakHttpJsonRequestFactory extends DefaultHttpJsonRequestFactor public HttpJsonRequest fromUrl(@NotNull String url) { return super.fromUrl(url) .setAuthorizationHeader( - "bearer " + EnvironmentContext.getCurrent().getSubject().getToken()); + "Bearer " + EnvironmentContext.getCurrent().getSubject().getToken()); } @Override public HttpJsonRequest fromLink(@NotNull Link link) { return super.fromLink(link) .setAuthorizationHeader( - "bearer " + EnvironmentContext.getCurrent().getSubject().getToken()); + "Bearer " + EnvironmentContext.getCurrent().getSubject().getToken()); } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java new file mode 100644 index 0000000000..625d0f649b --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java @@ -0,0 +1,53 @@ +/* + * 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 + */ +package org.eclipse.che.multiuser.keycloak.server; + +import java.io.IOException; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.che.api.core.ApiException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.rest.HttpJsonRequestFactory; +import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Fetches user profile from Keycloack server. + * + * @author David Festal + */ +@Singleton +public class KeycloakProfileRetriever { + private static final Logger LOG = LoggerFactory.getLogger(KeycloakProfileRetriever.class); + + private final String keyclockCurrentUserInfoUrl; + private final HttpJsonRequestFactory requestFactory; + + @Inject + public KeycloakProfileRetriever( + KeycloakSettings keycloakSettings, HttpJsonRequestFactory requestFactory) { + this.requestFactory = requestFactory; + this.keyclockCurrentUserInfoUrl = + keycloakSettings.get().get(KeycloakConstants.USERINFO_ENDPOINT_SETTING); + } + + public Map retrieveKeycloakAttributes() throws ServerException { + try { + return requestFactory.fromUrl(keyclockCurrentUserInfoUrl).request().asProperties(); + } catch (IOException | ApiException e) { + LOG.warn("Exception during retrieval of the Keycloak user profile", e); + throw new ServerException("Exception during retrieval of the Keycloak user profile", e); + } + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java index 2e87202d64..5c4b719857 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java @@ -13,6 +13,8 @@ package org.eclipse.che.multiuser.keycloak.server; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_DASHBOARD; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_IDE; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.GITHUB_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JWKS_ENDPOINT_SETTING; @@ -25,6 +27,7 @@ import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_ import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.TOKEN_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERINFO_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERNAME_CLAIM_SETTING; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_FIXED_REDIRECT_URLS_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING; import com.fasterxml.jackson.core.JsonFactory; @@ -54,6 +57,7 @@ public class KeycloakSettings { @Inject public KeycloakSettings( + @Named("che.api") String cheServerEndpoint, @Nullable @Named(JS_ADAPTER_URL_SETTING) String jsAdapterUrl, @Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL, @Nullable @Named(REALM_SETTING) String realm, @@ -62,7 +66,8 @@ public class KeycloakSettings { @Nullable @Named(USERNAME_CLAIM_SETTING) String usernameClaim, @Named(USE_NONCE_SETTING) boolean useNonce, @Nullable @Named(OSO_ENDPOINT_SETTING) String osoEndpoint, - @Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint) { + @Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint, + @Named(USE_FIXED_REDIRECT_URLS_SETTING) boolean useFixedRedirectUrls) { if (serverURL == null && oidcProvider == null) { throw new RuntimeException( @@ -139,6 +144,13 @@ public class KeycloakSettings { if (oidcProvider != null) { settings.put(OIDC_PROVIDER_SETTING, oidcProvider); + if (useFixedRedirectUrls) { + String rootUrl = + cheServerEndpoint.endsWith("/") ? cheServerEndpoint : cheServerEndpoint + "/"; + settings.put( + FIXED_REDIRECT_URL_FOR_DASHBOARD, rootUrl + "keycloak/oidcCallbackDashboard.html"); + settings.put(FIXED_REDIRECT_URL_FOR_IDE, rootUrl + "keycloak/oidcCallbackIde.html"); + } } settings.put(USE_NONCE_SETTING, Boolean.toString(useNonce)); if (jsAdapterUrl == null) { diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserManager.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserManager.java index e0da7826d2..283582d684 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserManager.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserManager.java @@ -11,6 +11,7 @@ */ package org.eclipse.che.multiuser.keycloak.server; +import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.System.currentTimeMillis; import static java.util.Collections.emptyList; import static org.eclipse.che.commons.lang.NameGenerator.generate; @@ -131,7 +132,7 @@ public class KeycloakUserManager extends PersonalAccountUserManager { * otherwise */ private User actualizeUserEmail(User actualUser, String email) throws ServerException { - if (actualUser.getEmail().equals(email)) { + if (isNullOrEmpty(email) || actualUser.getEmail().equals(email)) { return actualUser; } UserImpl update = new UserImpl(actualUser); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java index 1ef51dbb5c..f859039fde 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java @@ -13,22 +13,16 @@ package org.eclipse.che.multiuser.keycloak.server.dao; import static java.util.Objects.requireNonNull; -import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; -import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; -import org.eclipse.che.api.core.rest.HttpJsonRequestFactory; import org.eclipse.che.api.user.server.model.impl.ProfileImpl; import org.eclipse.che.api.user.server.spi.ProfileDao; import org.eclipse.che.commons.env.EnvironmentContext; -import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings; -import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.che.multiuser.keycloak.server.KeycloakProfileRetriever; /** * Fetches user profile from Keycloack server. @@ -37,17 +31,12 @@ import org.slf4j.LoggerFactory; * @author Sergii Leshchenko */ public class KeycloakProfileDao implements ProfileDao { - private static final Logger LOG = LoggerFactory.getLogger(KeycloakProfileDao.class); - private final String keyclockCurrentUserInfoUrl; - private final HttpJsonRequestFactory requestFactory; + private final KeycloakProfileRetriever keycloakProfileRetriever; @Inject - public KeycloakProfileDao( - KeycloakSettings keycloakSettings, HttpJsonRequestFactory requestFactory) { - this.requestFactory = requestFactory; - this.keyclockCurrentUserInfoUrl = - keycloakSettings.get().get(KeycloakConstants.USERINFO_ENDPOINT_SETTING); + public KeycloakProfileDao(KeycloakProfileRetriever keycloakProfileRetriever) { + this.keycloakProfileRetriever = keycloakProfileRetriever; } @Override @@ -74,15 +63,9 @@ public class KeycloakProfileDao implements ProfileDao { "It's not allowed to get foreign profile on current configured storage."); } - Map keycloakUserAttributes; // Retrieving own profile - try { - keycloakUserAttributes = - requestFactory.fromUrl(keyclockCurrentUserInfoUrl).request().asProperties(); - } catch (IOException | ApiException e) { - LOG.warn("Exception during retrieval of the Keycloak user profile", e); - throw new ServerException("Exception during retrieval of the Keycloak user profile", e); - } + Map keycloakUserAttributes = + keycloakProfileRetriever.retrieveKeycloakAttributes(); return new ProfileImpl(userId, mapAttributes(keycloakUserAttributes)); } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java index 63340eca98..70aa1a47bf 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakServletModule.java @@ -23,7 +23,7 @@ public class KeycloakServletModule extends ServletModule { private static final String KEYCLOAK_FILTER_PATHS = "^" // not equals to /keycloak/OIDCKeycloak.js - + "(?!/keycloak/OIDCKeycloak.js)" + + "(?!/keycloak/(OIDC|oidc)[^\\/]+$)" // not contains /docs/ (for swagger) + "(?!.*(/docs/))" // not ends with '/oauth/callback/' or '/keycloak/settings/' or '/system/state' diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java index 92fbd93e8a..cf9ba54683 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java @@ -38,6 +38,7 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.user.server.model.impl.UserImpl; import org.eclipse.che.commons.auth.token.RequestTokenExtractor; import org.eclipse.che.commons.env.EnvironmentContext; @@ -45,6 +46,7 @@ import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.commons.subject.SubjectImpl; import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject; import org.eclipse.che.multiuser.api.permission.server.PermissionChecker; +import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -59,6 +61,7 @@ public class KeycloakEnvironmentInitalizationFilterTest { @Mock private SignatureKeyManager keyManager; @Mock private KeycloakUserManager userManager; + @Mock private KeycloakProfileRetriever keycloakProfileRetriever; @Mock private KeycloakSettings keycloakSettings; @Mock private RequestTokenExtractor tokenExtractor; @Mock private PermissionChecker permissionChecker; @@ -70,6 +73,8 @@ public class KeycloakEnvironmentInitalizationFilterTest { @Mock private JwtParser jwtParser; private KeycloakEnvironmentInitalizationFilter filter; + private Map keycloakAttributes = new HashMap<>(); + private Map keycloakSettingsMap = new HashMap<>(); @BeforeMethod public void setUp() throws Exception { @@ -81,12 +86,22 @@ public class KeycloakEnvironmentInitalizationFilterTest { EnvironmentContext.setCurrent(context); filter = new KeycloakEnvironmentInitalizationFilter( - userManager, tokenExtractor, permissionChecker, keycloakSettings); + userManager, + keycloakProfileRetriever, + tokenExtractor, + permissionChecker, + keycloakSettings); Field parser = filter.getClass().getSuperclass().getDeclaredField("jwtParser"); parser.setAccessible(true); parser.set(filter, jwtParser); final KeyPair kp = new KeyPair(mock(PublicKey.class), mock(PrivateKey.class)); lenient().when(keyManager.getOrCreateKeyPair(anyString())).thenReturn(kp); + keycloakAttributes.clear(); + keycloakSettingsMap.clear(); + lenient() + .when(keycloakProfileRetriever.retrieveKeycloakAttributes()) + .thenReturn(keycloakAttributes); + lenient().when(keycloakSettings.get()).thenReturn(keycloakSettingsMap); } @Test @@ -102,7 +117,7 @@ public class KeycloakEnvironmentInitalizationFilterTest { } @Test - public void shouldThrowExceptionWhenNoEmailExists() throws Exception { + public void shouldThrowExceptionWhenNoEmailExistsAndUserDoesNotAlreadyExist() throws Exception { Map claimParams = new HashMap<>(); claimParams.put("preferred_username", "username"); @@ -111,6 +126,7 @@ public class KeycloakEnvironmentInitalizationFilterTest { // given when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token2"); when(request.getAttribute("token")).thenReturn(jwt); + when(userManager.getById(anyString())).thenThrow(NotFoundException.class); // when filter.doFilter(request, response, chain); @@ -124,6 +140,33 @@ public class KeycloakEnvironmentInitalizationFilterTest { verifyNoMoreInteractions(chain); } + @Test + public void shouldRetrieveTheEmailWhenItIsNotInJwtToken() throws Exception { + + Map claimParams = new HashMap<>(); + claimParams.put("preferred_username", "username"); + Claims claims = new DefaultClaims(claimParams).setSubject("id"); + DefaultJwt jwt = new DefaultJwt<>(new DefaultHeader(), claims); + UserImpl user = new UserImpl("id", "test@test.com", "username"); + keycloakSettingsMap.put(KeycloakConstants.USERNAME_CLAIM_SETTING, "preferred_username"); + // given + when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token"); + when(request.getAttribute("token")).thenReturn(jwt); + when(userManager.getById(anyString())).thenThrow(NotFoundException.class); + when(userManager.getOrCreateUser(anyString(), anyString(), anyString())).thenReturn(user); + keycloakAttributes.put("email", "test@test.com"); + + try { + // when + filter.doFilter(request, response, chain); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + + verify(userManager).getOrCreateUser("id", "test@test.com", "username"); + } + @Test public void shouldRefreshSubjectWhenTokensNotMatch() throws Exception { diff --git a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java index 66b73c573c..dc9a388d59 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java +++ b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java @@ -23,6 +23,8 @@ public class KeycloakConstants { public static final String OIDC_PROVIDER_SETTING = KEYCLOAK_SETTING_PREFIX + "oidc_provider"; public static final String USERNAME_CLAIM_SETTING = KEYCLOAK_SETTING_PREFIX + "username_claim"; public static final String USE_NONCE_SETTING = KEYCLOAK_SETTING_PREFIX + "use_nonce"; + public static final String USE_FIXED_REDIRECT_URLS_SETTING = + KEYCLOAK_SETTING_PREFIX + "use_fixed_redirect_urls"; public static final String JS_ADAPTER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "js_adapter_url"; public static final String ALLOWED_CLOCK_SKEW_SEC = KEYCLOAK_SETTING_PREFIX + "allowed_clock_skew_sec"; @@ -39,6 +41,11 @@ public class KeycloakConstants { KEYCLOAK_SETTING_PREFIX + "userinfo.endpoint"; public static final String GITHUB_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "github.endpoint"; + public static final String FIXED_REDIRECT_URL_FOR_DASHBOARD = + KEYCLOAK_SETTING_PREFIX + "redirect_url.dashboard"; + public static final String FIXED_REDIRECT_URL_FOR_IDE = + KEYCLOAK_SETTING_PREFIX + "redirect_url.ide"; + public static String getEndpoint(String apiEndpoint) { return apiEndpoint + KEYCLOAK_SETTINGS_ENDPOINT_PATH; } diff --git a/workspace-loader/src/index.ts b/workspace-loader/src/index.ts index 755db60dad..52884c6be6 100644 --- a/workspace-loader/src/index.ts +++ b/workspace-loader/src/index.ts @@ -81,30 +81,37 @@ export class KeycloakLoader { private initKeycloak(keycloakSettings: any): Promise { 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 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()); + 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'; + useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; } + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); keycloak - .init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce}) + .init({ + onLoad: 'login-required', + checkLoginIframe: false, + useNonce: useNonce, + scope: 'email profile', + redirectUri: keycloakSettings['che.keycloak.redirect_url.ide'] + }) .success(() => { resolve(keycloak); }) @@ -370,6 +377,7 @@ export class WorkspaceLoader { resolve(xhr); }).error(() => { console.log('Failed to refresh token'); + window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href); this.keycloak.login(); reject(); });