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();
});