Provide better compatibility with alternate OIDC providers (#11090)

Currently in Che there are still a number of requirements in upstream that are not required by the OIDC specification, so that Che still cannot be used with a number of OIDC compliant providers.
For example, in order to have Che working with the [`node-oidc-provider`](https://github.com/panva/node-oidc-provider), the following changes were necessary:

- Remove the requirement to have the email as a claim in the JWT access
token: this is not required the specification and is not supported by a
number of OIDC providers. Normally, the Id token contains such claims.

  So now if the email is not in the JWT token the first time the user connects to Che, ten the email is retrieved from the OIDC provider through its `user-profile` endpoint.

- Explicitely specify the the `openid email profile` scope when requesting the access token. Because OIDC providers, when answering to the `userInfo` endpoint, are expected to return claims that corresponds to the scopes of the access token. So if an access token has the `openid` scope only, the `userinfo` might return no claim at all (according to the specification).

  Until now it was working since keycloak allows adding claims to the returned tokens anyway.

- Allow supporting fixed redirect Uris: most OIDC providers support having a list of redirect URIs to come back to after the authorization step.  But these authorized Uris don't necessarily support wildcards or prefix. Che doesn't support this currently, and these changes introduce 2 fixed callback HTML pages that redirect to the Dashboard / IDE URL of the final page we want to come back to after authentication. This makes Che compatible with more OIDC providers

  We introduced a new boolean property to enable / disable fixed redirect URLs:
  `che.keycloak.use_fixed_redirect_urls` 
  whose default value is `false`

- The previous points required some light changes in the Keycloak Javascript adapter file, that we will submit as a PR to the Keycloak project. I, the meantime the `OIDCKeycloak.js` file is still used, but has been updated to be now based on the `keycloak.js` file of the last `4.5.0-final` Keycloak release. This will make this Keycloak PR easier to get accepted.

  Please keep in mind that this version upgrade only impacts the alternate OIDC provider case: when using a real Keycloak server, Che *always uses the `keycloak.js` file provided by the Keycloak server*. 


Signed-off-by: David Festal <dfestal@redhat.com>
6.19.x
David Festal 2018-10-10 20:52:35 +02:00 committed by GitHub
parent 97c703c719
commit 534a961e84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 702 additions and 204 deletions

View File

@ -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'

View File

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

View File

@ -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;
}

View File

@ -0,0 +1,20 @@
<!--
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
-->
<html>
<head>
<title>oidcCallback</title>
<script type="text/javascript" src="./oidcCallback.js"></script>
</head>
<body onload="redirectToInitialPage(false)"/>
</html>

View File

@ -0,0 +1,20 @@
<!--
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
-->
<html>
<head>
<title>oidcCallback</title>
<script type="text/javascript" src="./oidcCallback.js"></script>
</head>
<body onload="redirectToInitialPage(true)"/>
</html>

View File

@ -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<any>, reject: IRejectFn<any>) => {
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<any
resolve(xhr);
}).error(() => {
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;

View File

@ -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 {

View File

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

View File

@ -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;

View File

@ -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
########################################################################################
##### #####

View File

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

View File

@ -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'));
});

View File

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

View File

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

View File

@ -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");
}
}

View File

@ -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<String, String> 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);

View File

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

View File

@ -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 <dfestal@redhat.com>
*/
@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<String, String> 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);
}
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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<String, String> 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<String, String> keycloakUserAttributes =
keycloakProfileRetriever.retrieveKeycloakAttributes();
return new ProfileImpl(userId, mapAttributes(keycloakUserAttributes));
}

View File

@ -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'

View File

@ -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<String, String> keycloakAttributes = new HashMap<>();
private Map<String, String> 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<String, Object> 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<String, Object> claimParams = new HashMap<>();
claimParams.put("preferred_username", "username");
Claims claims = new DefaultClaims(claimParams).setSubject("id");
DefaultJwt<Claims> 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 {

View File

@ -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;
}

View File

@ -81,30 +81,37 @@ export class KeycloakLoader {
private initKeycloak(keycloakSettings: any): Promise<any> {
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();
});