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
parent
97c703c719
commit
534a961e84
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
########################################################################################
|
||||
##### #####
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue