From ff3459d2d31639c1003a0298ce958e454a285c81 Mon Sep 17 00:00:00 2001 From: David Festal Date: Fri, 23 Mar 2018 14:44:23 +0100 Subject: [PATCH] Support alternate OIDC providers, to prepare for the switch from Keycloak to `fabric8_auth` (#8650) Allow switching to an alternate OIDC provider (provided that it emits access tokens as JWT tokens). This is the implementation required in upstream Che, for issues redhat-developer/rh-che#502 and redhat-developer/rh-che#525 Signed-off-by: David Festal --- .../WEB-INF/classes/che/multiuser.properties | 19 + .../WEB-INF/classes/keycloak/OIDCKeycloak.js | 1379 +++++++++++++++++ dashboard/src/app/index.module.ts | 30 +- .../src/app/profile/profile.controller.ts | 8 + dashboard/src/app/profile/profile.html | 3 +- dockerfiles/init/manifests/che.env | 33 +- .../org/eclipse/che/ide/public/IDE.html | 30 +- .../che/multiuser/keycloak/ide/Keycloak.java | 27 +- .../keycloak/ide/KeycloakProvider.java | 15 +- .../che-multiuser-keycloak-server/pom.xml | 12 + .../server/AbstractKeycloakFilter.java | 4 +- .../server/KeycloakAuthenticationFilter.java | 130 +- .../server/KeycloakConfigurationService.java | 26 + .../keycloak/server/KeycloakSettings.java | 106 +- .../server/dao/KeycloakProfileDao.java | 8 +- .../keycloak/shared/KeycloakConstants.java | 6 + workspace-loader/src/index.ts | 29 +- 17 files changed, 1737 insertions(+), 128 deletions(-) create mode 100644 assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties index c7026956fd..9cef031807 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties @@ -99,9 +99,13 @@ che.organization.email.org_renamed_template=st-html-templates/organization_renam ##### KEYCLOACK CONFIGURATION ##### # Url to keycloak identity provider server +# Can be set to NULL only if `che.keycloak.oidcProvider` +# is used che.keycloak.auth_server_url=http://${CHE_HOST}:5050/auth # Keycloak realm is used to authenticate users +# Can be set to NULL only if `che.keycloak.oidcProvider` +# is used che.keycloak.realm=che # Keycloak client id in che.keycloak.realm that is used by dashboard, ide and cli to authenticate users @@ -117,3 +121,18 @@ che.keycloak.github.endpoint=NULL # The number of seconds to tolerate for clock skew when verifying exp or nbf claims. che.keycloak.allowed_clock_skew_sec=3 + +# Use the OIDC optional `nonce` feature to increase security. +che.keycloak.use_nonce=true + +# URL to the Keycloak Javascript adapter we want to use. +# if set to NULL, then the default used value is +# `${che.keycloak.auth_server_url}/js/keycloak.js`, +# or `/api/keycloak/OIDCKeycloak.js` +# if an alternate `oidc_provider` is used +che.keycloak.js_adapter_url=NULL + +# Base URL of an alternate OIDC provider that provides +# a discovery endpoint as detailed in the following specification +# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig +che.keycloak.oidc_provider=NULL diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js new file mode 100644 index 0000000000..d1fbd8e64d --- /dev/null +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/keycloak/OIDCKeycloak.js @@ -0,0 +1,1379 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +/* + * This is a modified version of the Keycloak Javascript Adapter whose + * original sources can be found there: + * https://github.com/keycloak/keycloak/blob/master/adapters/oidc/js/src/main/resources/keycloak.js + * + * Modifications allow using the Keycloak Javascript Adapter library with alternate OIDC-compliant providers, + * provided that they produce access tokens as JWT tokens with `iat` and `exp` claims. + */ + +(function( window, undefined ) { + + var Keycloak = function (config) { + if (!(this instanceof Keycloak)) { + return new Keycloak(config); + } + + var kc = this; + var adapter; + var refreshQueue = []; + var callbackStorage; + + var loginIframe = { + enable: true, + callbackList: [], + interval: 5 + }; + + var useNonce = true; + + kc.init = function (initOptions) { + kc.authenticated = false; + + callbackStorage = createCallbackStorage(); + + if (initOptions && initOptions.adapter === 'cordova') { + adapter = loadAdapter('cordova'); + } else if (initOptions && initOptions.adapter === 'default') { + adapter = loadAdapter(); + } else { + if (window.Cordova) { + adapter = loadAdapter('cordova'); + } else { + adapter = loadAdapter(); + } + } + + if (initOptions) { + if (typeof initOptions.useNonce !== 'undefined') { + useNonce = initOptions.useNonce; + } + + if (typeof initOptions.checkLoginIframe !== 'undefined') { + loginIframe.enable = initOptions.checkLoginIframe; + } + + if (initOptions.checkLoginIframeInterval) { + loginIframe.interval = initOptions.checkLoginIframeInterval; + } + + if (initOptions.onLoad === 'login-required') { + kc.loginRequired = true; + } + + if (initOptions.responseMode) { + if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') { + kc.responseMode = initOptions.responseMode; + } else { + throw 'Invalid value for responseMode'; + } + } + + if (initOptions.flow) { + switch (initOptions.flow) { + case 'standard': + kc.responseType = 'code'; + break; + case 'implicit': + kc.responseType = 'id_token token'; + break; + case 'hybrid': + kc.responseType = 'code id_token token'; + break; + default: + throw 'Invalid value for flow'; + } + kc.flow = initOptions.flow; + } + + if (initOptions.timeSkew != null) { + kc.timeSkew = initOptions.timeSkew; + } + } + + if (!kc.responseMode) { + kc.responseMode = 'fragment'; + } + if (!kc.responseType) { + kc.responseType = 'code'; + kc.flow = 'standard'; + } + + var promise = createPromise(); + + var initPromise = createPromise(); + initPromise.promise.success(function() { + kc.onReady && kc.onReady(kc.authenticated); + promise.setSuccess(kc.authenticated); + }).error(function(errorData) { + promise.setError(errorData); + }); + + var configPromise = loadConfig(config); + + function onLoad() { + var doLogin = function(prompt) { + if (!prompt) { + options.prompt = 'none'; + } + kc.login(options).success(function () { + initPromise.setSuccess(); + }).error(function () { + initPromise.setError(); + }); + } + + var options = {}; + switch (initOptions.onLoad) { + case 'check-sso': + if (loginIframe.enable) { + setupCheckLoginIframe().success(function() { + checkLoginIframe().success(function () { + doLogin(false); + }).error(function () { + initPromise.setSuccess(); + }); + }); + } else { + doLogin(false); + } + break; + case 'login-required': + doLogin(true); + break; + default: + throw 'Invalid value for onLoad'; + } + } + + function processInit() { + var callback = parseCallback(window.location.href); + + if (callback) { + setupCheckLoginIframe(); + window.history.replaceState({}, null, callback.newUrl); + processCallback(callback, initPromise); + return; + } else if (initOptions) { + if (initOptions.token && initOptions.refreshToken) { + setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); + + if (loginIframe.enable) { + setupCheckLoginIframe().success(function() { + checkLoginIframe().success(function () { + kc.onAuthSuccess && kc.onAuthSuccess(); + initPromise.setSuccess(); + }).error(function () { + setToken(null, null, null); + initPromise.setSuccess(); + }); + }); + } else { + kc.updateToken(-1).success(function() { + kc.onAuthSuccess && kc.onAuthSuccess(); + initPromise.setSuccess(); + }).error(function() { + kc.onAuthError && kc.onAuthError(); + if (initOptions.onLoad) { + onLoad(); + } else { + initPromise.setError(); + } + }); + } + } else if (initOptions.onLoad) { + onLoad(); + } else { + initPromise.setSuccess(); + } + } else { + initPromise.setSuccess(); + } + } + + configPromise.success(processInit); + configPromise.error(function() { + promise.setError(); + }); + + return promise.promise; + } + + kc.login = function (options) { + return adapter.login(options); + } + + kc.createLoginUrl = function(options) { + var state = createUUID(); + var nonce = createUUID(); + + var redirectUri = adapter.redirectUri(options); + + var callbackState = { + state: state, + nonce: nonce, + redirectUri: encodeURIComponent(redirectUri), + } + + if (options && options.prompt) { + callbackState.prompt = options.prompt; + } + + callbackStorage.add(callbackState); + + var baseUrl; + if (options && options.action == 'register') { + baseUrl = kc.endpoints.register(); + } else { + baseUrl = kc.endpoints.authorize(); + } + + var scope = (options && options.scope) ? "openid " + options.scope : "openid"; + + var url = baseUrl + + '?client_id=' + encodeURIComponent(kc.clientId) + + '&redirect_uri=' + encodeURIComponent(redirectUri) + + '&state=' + encodeURIComponent(state) + + '&response_mode=' + encodeURIComponent(kc.responseMode) + + '&response_type=' + encodeURIComponent(kc.responseType) + + '&scope=' + encodeURIComponent(scope); + if (useNonce) { + url = url + '&nonce=' + encodeURIComponent(nonce); + } + + if (options && options.prompt) { + url += '&prompt=' + encodeURIComponent(options.prompt); + } + + if (options && options.maxAge) { + url += '&max_age=' + encodeURIComponent(options.maxAge); + } + + if (options && options.loginHint) { + url += '&login_hint=' + encodeURIComponent(options.loginHint); + } + + if (options && options.idpHint) { + url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint); + } + + if (options && options.locale) { + url += '&ui_locales=' + encodeURIComponent(options.locale); + } + + return url; + } + + kc.logout = function(options) { + return adapter.logout(options); + } + + kc.createLogoutUrl = function(options) { + var url = kc.endpoints.logout() + + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false)); + + return url; + } + + kc.register = function (options) { + return adapter.register(options); + } + + kc.createRegisterUrl = function(options) { + if (!options) { + options = {}; + } + options.action = 'register'; + return kc.createLoginUrl(options); + } + + kc.createAccountUrl = function(options) { + var realm = getRealmUrl(); + var url = undefined; + if (typeof realm !== 'undefined') { + url = realm + + '/account' + + '?referrer=' + encodeURIComponent(kc.clientId) + + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options)); + } + return url; + } + + kc.accountManagement = function() { + return adapter.accountManagement(); + } + + kc.hasRealmRole = function (role) { + var access = kc.realmAccess; + return !!access && access.roles.indexOf(role) >= 0; + } + + kc.hasResourceRole = function(role, resource) { + if (!kc.resourceAccess) { + return false; + } + + var access = kc.resourceAccess[resource || kc.clientId]; + return !!access && access.roles.indexOf(role) >= 0; + } + + kc.loadUserProfile = function() { + var url = getRealmUrl() + '/account'; + var req = new XMLHttpRequest(); + req.open('GET', url, true); + req.setRequestHeader('Accept', 'application/json'); + req.setRequestHeader('Authorization', 'bearer ' + kc.token); + + var promise = createPromise(); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200) { + kc.profile = JSON.parse(req.responseText); + promise.setSuccess(kc.profile); + } else { + promise.setError(); + } + } + } + + req.send(); + + return promise.promise; + } + + kc.loadUserInfo = function() { + var url = kc.endpoints.userinfo(); + var req = new XMLHttpRequest(); + req.open('GET', url, true); + req.setRequestHeader('Accept', 'application/json'); + req.setRequestHeader('Authorization', 'bearer ' + kc.token); + + var promise = createPromise(); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200) { + kc.userInfo = JSON.parse(req.responseText); + promise.setSuccess(kc.userInfo); + } else { + promise.setError(); + } + } + } + + req.send(); + + return promise.promise; + } + + kc.isTokenExpired = function(minValidity) { + if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) { + throw 'Not authenticated'; + } + + if (kc.timeSkew == null) { + console.info('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set'); + return true; + } + + var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew; + if (minValidity) { + expiresIn -= minValidity; + } + return expiresIn < 0; + } + + kc.updateToken = function(minValidity) { + var promise = createPromise(); + + if (!kc.refreshToken) { + promise.setError(); + return promise.promise; + } + + minValidity = minValidity || 5; + + var exec = function() { + var refreshToken = false; + if (minValidity == -1) { + refreshToken = true; + console.info('[KEYCLOAK] Refreshing token: forced refresh'); + } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) { + refreshToken = true; + console.info('[KEYCLOAK] Refreshing token: token expired'); + } + + if (!refreshToken) { + promise.setSuccess(false); + } else { + var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken; + var url = kc.endpoints.token(); + + refreshQueue.push(promise); + + if (refreshQueue.length == 1) { + var req = new XMLHttpRequest(); + req.open('POST', url, true); + req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + req.withCredentials = true; + + if (kc.clientId && kc.clientSecret) { + req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); + } else { + params += '&client_id=' + encodeURIComponent(kc.clientId); + } + + var timeLocal = new Date().getTime(); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200) { + console.info('[KEYCLOAK] Token refreshed'); + + timeLocal = (timeLocal + new Date().getTime()) / 2; + + var tokenResponse = JSON.parse(req.responseText); + + setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal); + + kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess(); + for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { + p.setSuccess(true); + } + } else { + console.warn('[KEYCLOAK] Failed to refresh token'); + + kc.onAuthRefreshError && kc.onAuthRefreshError(); + for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { + p.setError(true); + } + } + } + }; + + req.send(params); + } + } + } + + if (loginIframe.enable) { + var iframePromise = checkLoginIframe(); + iframePromise.success(function() { + exec(); + }).error(function() { + promise.setError(); + }); + } else { + exec(); + } + + return promise.promise; + } + + kc.clearToken = function() { + if (kc.token) { + setToken(null, null, null); + kc.onAuthLogout && kc.onAuthLogout(); + if (kc.loginRequired) { + kc.login(); + } + } + } + + function getRealmUrl() { + if (typeof kc.authServerUrl !== 'undefined') { + if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') { + return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm); + } else { + return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm); + } + } else { + return undefined; + } + } + + function getOrigin() { + if (!window.location.origin) { + return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); + } else { + return window.location.origin; + } + } + + function processCallback(oauth, promise) { + var code = oauth.code; + var error = oauth.error; + var prompt = oauth.prompt; + + var timeLocal = new Date().getTime(); + + if (error) { + if (prompt != 'none') { + var errorData = { error: error, error_description: oauth.error_description }; + kc.onAuthError && kc.onAuthError(errorData); + promise && promise.setError(errorData); + } else { + promise && promise.setSuccess(); + } + return; + } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) { + authSuccess(oauth.access_token, null, oauth.id_token, true); + } + + if ((kc.flow != 'implicit') && code) { + var params = 'code=' + code + '&grant_type=authorization_code'; + var url = kc.endpoints.token(); + + var req = new XMLHttpRequest(); + req.open('POST', url, true); + req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + if (kc.clientId && kc.clientSecret) { + req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); + } else { + params += '&client_id=' + encodeURIComponent(kc.clientId); + } + + params += '&redirect_uri=' + oauth.redirectUri; + + req.withCredentials = true; + + req.onreadystatechange = function() { + if (req.readyState == 4) { + if (req.status == 200) { + + 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(); + } + } + }; + + req.send(params); + } + + function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) { + timeLocal = (timeLocal + new Date().getTime()) / 2; + + setToken(accessToken, refreshToken, idToken, timeLocal); + + if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) || + (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) || + (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) { + + console.info('[KEYCLOAK] Invalid nonce, clearing token'); + kc.clearToken(); + promise && promise.setError(); + } else { + if (fulfillPromise) { + kc.onAuthSuccess && kc.onAuthSuccess(); + promise && promise.setSuccess(); + } + } + } + + } + + function loadConfig(url) { + var promise = createPromise(); + var configUrl; + + if (!config) { + configUrl = 'keycloak.json'; + } else if (typeof config === 'string') { + configUrl = config; + } + + function setupOidcEndoints(oidcConfiguration) { + if (! oidcConfiguration) { + kc.endpoints = { + authorize: function() { + return getRealmUrl() + '/protocol/openid-connect/auth'; + }, + token: function() { + return getRealmUrl() + '/protocol/openid-connect/token'; + }, + logout: function() { + return getRealmUrl() + '/protocol/openid-connect/logout'; + }, + checkSessionIframe: function() { + return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; + }, + register: function() { + return getRealmUrl() + '/protocol/openid-connect/registrations'; + }, + userinfo: function() { + return getRealmUrl() + '/protocol/openid-connect/userinfo'; + } + }; + } else { + kc.endpoints = { + authorize: function() { + return oidcConfiguration.authorization_endpoint; + }, + token: function() { + return oidcConfiguration.token_endpoint; + }, + logout: function() { + if (!oidcConfiguration.end_session_endpoint) { + throw "Not supported by the OIDC server"; + } + return oidcConfiguration.end_session_endpoint; + }, + checkSessionIframe: function() { + if (!oidcConfiguration.check_session_iframe) { + throw "Not supported by the OIDC server"; + } + return oidcConfiguration.check_session_iframe; + }, + register: function() { + throw 'Redirection to "Register user" page not supported in standard OIDC mode'; + }, + userinfo: function() { + if (!oidcConfiguration.userinfo_endpoint) { + throw "Not supported by the OIDC server"; + } + return oidcConfiguration.userinfo_endpoint; + } + } + } + } + + if (configUrl) { + var req = new XMLHttpRequest(); + req.open('GET', configUrl, true); + req.setRequestHeader('Accept', 'application/json'); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200 || fileLoaded(req)) { + var config = JSON.parse(req.responseText); + + kc.authServerUrl = config['auth-server-url']; + kc.realm = config['realm']; + kc.clientId = config['resource']; + kc.clientSecret = (config['credentials'] || {})['secret']; + setupOidcEndoints(null); + promise.setSuccess(); + } else { + promise.setError(); + } + } + }; + + req.send(); + } else { + if (!config.clientId) { + throw 'clientId missing'; + } + + kc.clientId = config.clientId; + kc.clientSecret = (config.credentials || {}).secret; + + var oidcProvider = config['oidcProvider']; + if (!oidcProvider) { + if (!config['url']) { + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + if (scripts[i].src.match(/.*keycloak\.js/)) { + config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); + break; + } + } + } + if (!config.realm) { + throw 'realm missing'; + } + + kc.authServerUrl = config.url; + kc.realm = config.realm; + setupOidcEndoints(null); + promise.setSuccess(); + } else { + if (typeof oidcProvider === 'string') { + var oidcProviderConfigUrl; + if (oidcProvider.charAt(oidcProvider.length - 1) == '/') { + oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration'; + } else { + oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration'; + } + var req = new XMLHttpRequest(); + req.open('GET', oidcProviderConfigUrl, true); + req.setRequestHeader('Accept', 'application/json'); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + if (req.status == 200 || fileLoaded(req)) { + var oidcProviderConfig = JSON.parse(req.responseText); + setupOidcEndoints(oidcProviderConfig); + promise.setSuccess(); + } else { + promise.setError(); + } + } + }; + + req.send(); + } else { + setupOidcEndoints(oidcProvider); + promise.setSuccess(); + } + } + } + + return promise.promise; + } + + function fileLoaded(xhr) { + return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:'); + } + + function setToken(token, refreshToken, idToken, timeLocal) { + if (kc.tokenTimeoutHandle) { + clearTimeout(kc.tokenTimeoutHandle); + kc.tokenTimeoutHandle = null; + } + + if (refreshToken) { + kc.refreshToken = refreshToken; + kc.refreshTokenParsed = decodeToken(refreshToken); + } else { + delete kc.refreshToken; + delete kc.refreshTokenParsed; + } + + if (idToken) { + kc.idToken = idToken; + kc.idTokenParsed = decodeToken(idToken); + } else { + delete kc.idToken; + delete kc.idTokenParsed; + } + + if (token) { + kc.token = token; + kc.tokenParsed = decodeToken(token); + kc.sessionId = kc.tokenParsed.session_state; + kc.authenticated = true; + kc.subject = kc.tokenParsed.sub; + kc.realmAccess = kc.tokenParsed.realm_access; + kc.resourceAccess = kc.tokenParsed.resource_access; + + if (timeLocal) { + kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; + } + + if (kc.timeSkew != null) { + console.info('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds'); + + if (kc.onTokenExpired) { + var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000; + console.info('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s'); + if (expiresIn <= 0) { + kc.onTokenExpired(); + } else { + kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn); + } + } + } + } else { + delete kc.token; + delete kc.tokenParsed; + delete kc.subject; + delete kc.realmAccess; + delete kc.resourceAccess; + + kc.authenticated = false; + } + } + + function decodeToken(str) { + str = str.split('.')[1]; + + str = str.replace('/-/g', '+'); + str = str.replace('/_/g', '/'); + switch (str.length % 4) + { + case 0: + break; + case 2: + str += '=='; + break; + case 3: + str += '='; + break; + default: + throw 'Invalid token'; + } + + str = (str + '===').slice(0, str.length + (str.length % 4)); + str = str.replace(/-/g, '+').replace(/_/g, '/'); + + str = decodeURIComponent(escape(atob(str))); + + str = JSON.parse(str); + return str; + } + + function createUUID() { + var s = []; + var hexDigits = '0123456789abcdef'; + for (var i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = '4'; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = '-'; + var uuid = s.join(''); + return uuid; + } + + kc.callback_id = 0; + + function createCallbackId() { + var id = ''; + return id; + + } + + function parseCallback(url) { + var oauth = new CallbackParser(url, kc.responseMode).parseUri(); + var oauthState = callbackStorage.get(oauth.state); + + if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) { + oauth.redirectUri = oauthState.redirectUri; + oauth.storedNonce = oauthState.nonce; + oauth.prompt = oauthState.prompt; + + if (oauth.fragment) { + oauth.newUrl += '#' + oauth.fragment; + } + + return oauth; + } + } + + function createPromise() { + var p = { + setSuccess: function(result) { + p.success = true; + p.result = result; + if (p.successCallback) { + p.successCallback(result); + } + }, + + setError: function(result) { + p.error = true; + p.result = result; + if (p.errorCallback) { + p.errorCallback(result); + } + }, + + promise: { + success: function(callback) { + if (p.success) { + callback(p.result); + } else if (!p.error) { + p.successCallback = callback; + } + return p.promise; + }, + error: function(callback) { + if (p.error) { + callback(p.result); + } else if (!p.success) { + p.errorCallback = callback; + } + return p.promise; + } + } + } + return p; + } + + function setupCheckLoginIframe() { + var promise = createPromise(); + + if (!loginIframe.enable) { + promise.setSuccess(); + return promise.promise; + } + + if (loginIframe.iframe) { + promise.setSuccess(); + return promise.promise; + } + + var iframe = document.createElement('iframe'); + loginIframe.iframe = iframe; + + iframe.onload = function() { + var authUrl = kc.authorize(); + if (authUrl.charAt(0) === '/') { + loginIframe.iframeOrigin = getOrigin(); + } else { + loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8)); + } + promise.setSuccess(); + + setTimeout(check, loginIframe.interval * 1000); + } + + var src = kc.checkSessionIframe(); + iframe.setAttribute('src', src ); + iframe.style.display = 'none'; + document.body.appendChild(iframe); + + var messageCallback = function(event) { + if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) { + return; + } + + if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) { + return; + } + + + if (event.data != 'unchanged') { + kc.clearToken(); + } + + var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length); + + for (var i = callbacks.length - 1; i >= 0; --i) { + var promise = callbacks[i]; + if (event.data == 'unchanged') { + promise.setSuccess(); + } else { + promise.setError(); + } + } + }; + + window.addEventListener('message', messageCallback, false); + + var check = function() { + checkLoginIframe(); + if (kc.token) { + setTimeout(check, loginIframe.interval * 1000); + } + }; + + return promise.promise; + } + + function checkLoginIframe() { + var promise = createPromise(); + + if (loginIframe.iframe && loginIframe.iframeOrigin ) { + var msg = kc.clientId + ' ' + kc.sessionId; + loginIframe.callbackList.push(promise); + var origin = loginIframe.iframeOrigin; + if (loginIframe.callbackList.length == 1) { + loginIframe.iframe.contentWindow.postMessage(msg, origin); + } + } else { + promise.setSuccess(); + } + + return promise.promise; + } + + function loadAdapter(type) { + if (!type || type == 'default') { + return { + login: function(options) { + window.location.href = kc.createLoginUrl(options); + return createPromise().promise; + }, + + logout: function(options) { + window.location.href = kc.createLogoutUrl(options); + return createPromise().promise; + }, + + register: function(options) { + window.location.href = kc.createRegisterUrl(options); + return createPromise().promise; + }, + + accountManagement : function() { + var accountUrl = kc.createAccountUrl(); + if (typeof accountUrl !== 'undefined') { + window.location.href = accountUrl; + } else { + throw "Not supported by the OIDC server"; + } + return createPromise().promise; + }, + + redirectUri: function(options, encodeHash) { + if (arguments.length == 1) { + encodeHash = true; + } + + if (options && options.redirectUri) { + return options.redirectUri; + } 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; + } + } + }; + } + + if (type == 'cordova') { + loginIframe.enable = false; + + return { + login: function(options) { + var promise = createPromise(); + + var o = 'location=no'; + if (options && options.prompt == 'none') { + o += ',hidden=yes'; + } + + var loginUrl = kc.createLoginUrl(options); + var ref = window.open(loginUrl, '_blank', o); + + var completed = false; + + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + var callback = parseCallback(event.url); + processCallback(callback, promise); + ref.close(); + completed = true; + } + }); + + ref.addEventListener('loaderror', function(event) { + if (!completed) { + if (event.url.indexOf('http://localhost') == 0) { + var callback = parseCallback(event.url); + processCallback(callback, promise); + ref.close(); + completed = true; + } else { + promise.setError(); + ref.close(); + } + } + }); + + 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 error; + + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + } + }); + + ref.addEventListener('loaderror', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + } else { + error = true; + ref.close(); + } + }); + + ref.addEventListener('exit', function(event) { + if (error) { + promise.setError(); + } else { + kc.clearToken(); + promise.setSuccess(); + } + }); + + return promise.promise; + }, + + register : function() { + var registerUrl = kc.createRegisterUrl(); + var ref = window.open(registerUrl, '_blank', 'location=no'); + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + } + }); + }, + + accountManagement : function() { + var accountUrl = kc.createAccountUrl(); + if (typeof accountUrl !== 'undefined') { + var ref = window.open(accountUrl, '_blank', 'location=no'); + ref.addEventListener('loadstart', function(event) { + if (event.url.indexOf('http://localhost') == 0) { + ref.close(); + } + }); + } else { + throw "Not supported by the OIDC server"; + } + }, + + redirectUri: function(options) { + return 'http://localhost'; + } + } + } + + throw 'invalid adapter type: ' + type; + } + + var LocalStorage = function() { + if (!(this instanceof LocalStorage)) { + return new LocalStorage(); + } + + localStorage.setItem('kc-test', 'test'); + localStorage.removeItem('kc-test'); + + var cs = this; + + function clearExpired() { + var time = new Date().getTime(); + for (var i = 0; i < localStorage.length; i++) { + var key = localStorage.key(i); + if (key && key.indexOf('kc-callback-') == 0) { + var value = localStorage.getItem(key); + if (value) { + try { + var expires = JSON.parse(value).expires; + if (!expires || expires < time) { + localStorage.removeItem(key); + } + } catch (err) { + localStorage.removeItem(key); + } + } + } + } + } + + cs.get = function(state) { + if (!state) { + return; + } + + var key = 'kc-callback-' + state; + var value = localStorage.getItem(key); + if (value) { + localStorage.removeItem(key); + value = JSON.parse(value); + } + + clearExpired(); + return value; + }; + + cs.add = function(state) { + clearExpired(); + + var key = 'kc-callback-' + state.state; + state.expires = new Date().getTime() + (60 * 60 * 1000); + localStorage.setItem(key, JSON.stringify(state)); + }; + }; + + var CookieStorage = function() { + if (!(this instanceof CookieStorage)) { + return new CookieStorage(); + } + + var cs = this; + + cs.get = function(state) { + if (!state) { + return; + } + + var value = getCookie('kc-callback-' + state); + setCookie('kc-callback-' + state, '', cookieExpiration(-100)); + if (value) { + return JSON.parse(value); + } + }; + + cs.add = function(state) { + setCookie('kc-callback-' + state.state, JSON.stringify(state), cookieExpiration(60)); + }; + + cs.removeItem = function(key) { + setCookie(key, '', cookieExpiration(-100)); + }; + + var cookieExpiration = function (minutes) { + var exp = new Date(); + exp.setTime(exp.getTime() + (minutes*60*1000)); + return exp; + }; + + var getCookie = function (key) { + var name = key + '='; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ''; + }; + + var setCookie = function (key, value, expirationDate) { + var cookie = key + '=' + value + '; ' + + 'expires=' + expirationDate.toUTCString() + '; '; + document.cookie = cookie; + } + }; + + function createCallbackStorage() { + try { + return new LocalStorage(); + } catch (err) { + } + + 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" ) { + module.exports = Keycloak; + } else { + window.Keycloak = Keycloak; + + if ( typeof define === "function" && define.amd ) { + define( "keycloak", [], function () { return Keycloak; } ); + } + } +})( window ); \ No newline at end of file diff --git a/dashboard/src/app/index.module.ts b/dashboard/src/app/index.module.ts index c767e29c87..1fccd4075b 100755 --- a/dashboard/src/app/index.module.ts +++ b/dashboard/src/app/index.module.ts @@ -48,11 +48,19 @@ window.name = 'NG_DEFER_BOOTSTRAP!'; declare const Keycloak: Function; function buildKeycloakConfig(keycloakSettings: any) { - return { - url: keycloakSettings['che.keycloak.auth_server_url'], - realm: keycloakSettings['che.keycloak.realm'], - 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'] + }; + } } interface IResolveFn { (value: T | PromiseLike): Promise; @@ -64,18 +72,18 @@ function keycloakLoad(keycloakSettings: any) { return new Promise((resolve: IResolveFn, reject: IRejectFn) => { const script = document.createElement('script'); script.async = true; - script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js'; + script.src = keycloakSettings['che.keycloak.js_adapter_url']; script.addEventListener('load', resolve); script.addEventListener('error', () => reject('Error loading script.')); script.addEventListener('abort', () => reject('Script loading aborted.')); document.head.appendChild(script); }); } -function keycloakInit(keycloakConfig: any) { +function keycloakInit(keycloakConfig: any, theUseNonce: boolean) { return new Promise((resolve: IResolveFn, reject: IRejectFn) => { const keycloak = Keycloak(keycloakConfig); keycloak.init({ - onLoad: 'login-required', checkLoginIframe: false + onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce }).success(() => { resolve(keycloak); }).error((error: any) => { @@ -101,7 +109,11 @@ angular.element(document).ready(() => { // load Keycloak return keycloakLoad(keycloakSettings).then(() => { // init Keycloak - return keycloakInit(keycloakAuth.config); + var useNonce: boolean; + if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { + useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; + } + return keycloakInit(keycloakAuth.config, useNonce); }).then((keycloak: any) => { keycloakAuth.isPresent = true; keycloakAuth.keycloak = keycloak; diff --git a/dashboard/src/app/profile/profile.controller.ts b/dashboard/src/app/profile/profile.controller.ts index f8dbbc59c5..a896c145da 100644 --- a/dashboard/src/app/profile/profile.controller.ts +++ b/dashboard/src/app/profile/profile.controller.ts @@ -47,4 +47,12 @@ export class ProfileController { editProfile(): void { this.$window.open(this.profileUrl); } + + /** + * Edit profile - redirects to proper page. + */ + get cannotEdit: boolean { + return !this.profileUrl; + } + } diff --git a/dashboard/src/app/profile/profile.html b/dashboard/src/app/profile/profile.html index e4bd05753a..3ea7af2884 100644 --- a/dashboard/src/app/profile/profile.html +++ b/dashboard/src/app/profile/profile.html @@ -52,6 +52,7 @@ + ng-click="profileController.editProfile()" + ng-disabled="profileController.cannotEdit"> diff --git a/dockerfiles/init/manifests/che.env b/dockerfiles/init/manifests/che.env index 818d41e169..fe2249193a 100644 --- a/dockerfiles/init/manifests/che.env +++ b/dockerfiles/init/manifests/che.env @@ -563,12 +563,39 @@ CHE_INFRA_OPENSHIFT_TLS_ENABLED=false # #CHE_KEYCLOAK_OSO_ENDPOINT=NULL #CHE_KEYCLOAK_GITHUB_ENDPOINT=NULL -#CHE_KEYCLOAK_AUTH__SERVER__URL=http://172.17.0.1:5050/auth -#CHE_KEYCLOAK_REALM=che -#CHE_KEYCLOAK_CLIENT__ID=che-public #CHE_KEYCLOAK_ALLOWED__CLOCK__SKEW__SEC=3 #CHE_KEYCLOAK_ADMIN_REQUIRE_UPDATE_PASSWORD=true +# +# Url to keycloak identity provider server +# Can be set to NULL only if `CHE_KEYCLOAK_OIDC__PROVIDER` +# is used +#CHE_KEYCLOAK_AUTH__SERVER__URL=http://172.17.0.1:5050/auth + +# Keycloak realm is used to authenticate users +# Can be set to NULL only if `CHE_KEYCLOAK_OIDC__PROVIDER` +# is used +#CHE_KEYCLOAK_REALM=che + +# Keycloak or OIDC client id used by dashboard, +# ide and cli to authenticate users +#CHE_KEYCLOAK_CLIENT__ID=che-public + +# Use the OIDC optional `nonce` feature to increase security. +#CHE_KEYCLOAK_USE__NONCE=true + +# URL to the Keycloak Javascript adapter we want to use. +# if set to NULL, then the default used value is +# `${CHE_KEYCLOAK_AUTH__SERVER__URL}/js/keycloak.js`, +# or `/api/keycloak/OIDCKeycloak.js` +# if an alternate `oidc_provider` is used +#CHE_KEYCLOAK_JS__ADAPTER__URL=NULL + +# Base URL of an alternate OIDC provider that provides +# a discovery endpoint as detailed in the following specification +# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig +#CHE_KEYCLOAK_OIDC__PROVIDER=NULL + ######################################################################################## ##### ##### diff --git a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html index da07f27d38..36f1e6277a 100644 --- a/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html +++ b/ide/che-ide-gwt-app/src/main/resources/org/eclipse/che/ide/public/IDE.html @@ -96,7 +96,7 @@ script.type = "text/javascript"; script.language = "javascript"; script.async = true; - script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js'; + script.src = keycloakSettings['che.keycloak.js_adapter_url']; script.onload = function() { Loader.initKeycloak(keycloakSettings); @@ -114,16 +114,32 @@ * Initialize keycloak and load the IDE */ this.initKeycloak = function(keycloakSettings) { - var keycloak = Keycloak({ - url: keycloakSettings['che.keycloak.auth_server_url'], - realm: keycloakSettings['che.keycloak.realm'], - clientId: keycloakSettings['che.keycloak.client_id'] - }); + 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'] + }; + } + } + var keycloak = Keycloak(keycloakConfig()); window['_keycloak'] = keycloak; + var useNonce; + if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { + useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; + } + keycloak - .init({onLoad: 'login-required', checkLoginIframe: false}) + .init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce}) .success(function(authenticated) { Loader.startLoading(); }) diff --git a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java index 89670a922e..f36385f2ea 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java +++ b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/Keycloak.java @@ -39,17 +39,30 @@ public final class Keycloak extends JavaScriptObject { }-*/; public static native Promise init( - String theUrl, String theRealm, String theClientId) /*-{ + String theUrl, + String theRealm, + String theClientId, + String theOidcProvider, + boolean theUseNonce) /*-{ return new Promise(function (resolve, reject) { try { console.log('[Keycloak] Initializing'); - var keycloak = $wnd.Keycloak({ - url: theUrl, - realm: theRealm, - clientId: theClientId - }); + var config; + if(!theOidcProvider) { + config = { + url: theUrl, + realm: theRealm, + clientId: theClientId + }; + } else { + config = { + oidcProvider: theOidcProvider, + clientId: theClientId + }; + } + var keycloak = $wnd.Keycloak(config); $wnd['_keycloak'] = keycloak; - keycloak.init({onLoad: 'login-required', checkLoginIframe: false}) + keycloak.init({onLoad: 'login-required', checkLoginIframe: false, useNonce: theUseNonce}) .success(function (authenticated) { resolve(keycloak); }) diff --git a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java index 00baec05bc..4eaa007844 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java +++ b/multiuser/keycloak/che-multiuser-keycloak-ide/src/main/java/org/eclipse/che/multiuser/keycloak/ide/KeycloakProvider.java @@ -12,7 +12,10 @@ 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.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; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING; import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.JavaScriptObject; @@ -50,13 +53,15 @@ public class KeycloakProvider { return; } + String keycloakServerUrl = settings.get(AUTH_SERVER_URL_SETTING); + String jsAdapterUrl = settings.get(JS_ADAPTER_URL_SETTING); + keycloak = CallbackPromiseHelper.createFromCallback( new CallbackPromiseHelper.Call() { @Override public void makeCall(final Callback callback) { - ScriptInjector.fromUrl( - settings.get(AUTH_SERVER_URL_SETTING) + "/js/keycloak.js") + ScriptInjector.fromUrl(jsAdapterUrl) .setCallback( new Callback() { @Override @@ -76,9 +81,11 @@ public class KeycloakProvider { .thenPromise( (v) -> Keycloak.init( - settings.get(AUTH_SERVER_URL_SETTING), + keycloakServerUrl, settings.get(REALM_SETTING), - settings.get(CLIENT_ID_SETTING))); + settings.get(CLIENT_ID_SETTING), + settings.get(OIDC_PROVIDER_SETTING), + Boolean.valueOf(settings.get(USE_NONCE_SETTING)).booleanValue())); Log.debug(getClass(), "Keycloak init complete: ", this); } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml index 1c4c2a25e9..9078c51f61 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml +++ b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml @@ -25,6 +25,18 @@ ${project.build.directory}/generated-sources/dto/ + + com.auth0 + jwks-rsa + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + com.google.code.gson gson diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java index 3d01be5b0f..d753ecc8e9 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java @@ -24,7 +24,9 @@ import javax.servlet.http.HttpServletRequest; public abstract class AbstractKeycloakFilter implements Filter { protected boolean shouldSkipAuthentication(HttpServletRequest request, String token) { - return token != null && token.startsWith("machine"); + return (token != null && token.startsWith("machine")) + || (request.getRequestURI() != null + && request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js")); } @Override diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java index 3bac063ba2..7df25b8d41 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java @@ -10,25 +10,23 @@ */ package org.eclipse.che.multiuser.keycloak.server; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import com.auth0.jwk.GuavaCachedJwkProvider; +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkException; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureException; -import java.io.BufferedReader; +import io.jsonwebtoken.SigningKeyResolverAdapter; import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.reflect.Type; -import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; +import java.security.Key; import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; @@ -45,27 +43,25 @@ import org.slf4j.LoggerFactory; @Singleton public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter { - private static final Gson GSON = new Gson(); - private static final Type STRING_MAP_TYPE = new TypeToken>() {}.getType(); - private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class); - private String authServerUrl; - private String realm; + private String jwksUrl; private long allowedClockSkewSec; - private PublicKey publicKey = null; private RequestTokenExtractor tokenExtractor; + private JwkProvider jwkProvider; @Inject public KeycloakAuthenticationFilter( - @Named(KeycloakConstants.AUTH_SERVER_URL_SETTING) String authServerUrl, - @Named(KeycloakConstants.REALM_SETTING) String realm, + KeycloakSettings keycloakSettings, @Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec, - RequestTokenExtractor tokenExtractor) { - this.authServerUrl = authServerUrl; - this.realm = realm; + RequestTokenExtractor tokenExtractor) + throws MalformedURLException { + this.jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING); this.allowedClockSkewSec = allowedClockSkewSec; this.tokenExtractor = tokenExtractor; + if (jwksUrl != null) { + this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl))); + } } @Override @@ -91,69 +87,55 @@ public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter { jwt = Jwts.parser() .setAllowedClockSkewSeconds(allowedClockSkewSec) - .setSigningKey(getJwtPublicKey(false)) + .setSigningKeyResolver( + new SigningKeyResolverAdapter() { + @Override + public Key resolveSigningKey( + @SuppressWarnings("rawtypes") JwsHeader header, Claims claims) { + try { + return getJwtPublicKey(header); + } catch (JwkException e) { + throw new JwtException( + "Error during the retrieval of the public key during JWT token validation", + e); + } + } + }) .parseClaimsJws(token); LOG.debug("JWT = ", jwt); // OK, we can trust this JWT - } catch (SignatureException - | NoSuchAlgorithmException - | InvalidKeySpecException - | IllegalArgumentException e) { + } catch (SignatureException | IllegalArgumentException e) { // don't trust the JWT! LOG.error("Failed verifying the JWT token", e); - try { - LOG.info("Retrying after updating the public key", e); - jwt = - Jwts.parser() - .setAllowedClockSkewSeconds(allowedClockSkewSec) - .setSigningKey(getJwtPublicKey(true)) - .parseClaimsJws(token); - LOG.debug("JWT = ", jwt); - // OK, we can trust this JWT - } catch (SignatureException - | NoSuchAlgorithmException - | InvalidKeySpecException - | IllegalArgumentException ee) { - // don't trust the JWT! - LOG.error("Failed verifying the JWT token after public key update", e); - send403(res); - return; - } + send403(res); + return; } request.setAttribute("token", jwt); chain.doFilter(req, res); } - private synchronized PublicKey getJwtPublicKey(boolean reset) - throws NoSuchAlgorithmException, InvalidKeySpecException { - if (reset) { - publicKey = null; + private synchronized PublicKey getJwtPublicKey(JwsHeader header) throws JwkException { + String kid = header.getKeyId(); + if (kid == null) { + LOG.warn( + "'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys"); + return null; } - if (publicKey == null) { - HttpURLConnection conn = null; - try { - URL url = new URL(authServerUrl + "/realms/" + realm); - LOG.info("Pulling realm public key from URL : {}", url); - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - Map realmSettings; - try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { - realmSettings = GSON.fromJson(in, STRING_MAP_TYPE); - } - String encodedPublicKey = realmSettings.get("public_key"); - byte[] decoded = Base64.getDecoder().decode(encodedPublicKey); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded); - KeyFactory kf = KeyFactory.getInstance("RSA"); - publicKey = kf.generatePublic(keySpec); - } catch (IOException e) { - LOG.error("Exception during retrieval of the Keycloak realm public key", e); - } finally { - if (conn != null) { - conn.disconnect(); - } - } + String alg = header.getAlgorithm(); + if (alg == null) { + LOG.warn( + "'alg' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys"); + return null; } - return publicKey; + + if (jwkProvider == null) { + LOG.warn( + "JWK provider is not available: This is not possible to validate the token with OIDC provider keys.\n" + + "Please look into the startup logs to find out the root cause"); + return null; + } + Jwk jwk = jwkProvider.get(kid); + return jwk.getPublicKey(); } private void send403(ServletResponse res) throws IOException { diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java index e5f98f9ef2..db0c110c6d 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakConfigurationService.java @@ -12,6 +12,11 @@ package org.eclipse.che.multiuser.keycloak.server; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; @@ -43,4 +48,25 @@ public class KeycloakConfigurationService extends Service { public Map settings() { return keycloakSettings.get(); } + + @GET + @Path("/OIDCKeycloak.js") + @Produces("text/javascript") + public String javascriptAdapter() throws IOException { + URL resource = + Thread.currentThread().getContextClassLoader().getResource("keycloak/OIDCKeycloak.js"); + if (resource != null) { + URLConnection conn = resource.openConnection(); + try (InputStream is = conn.getInputStream(); + ByteArrayOutputStream os = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) != -1) { + os.write(buffer, 0, length); + } + return os.toString("UTF-8"); + } + } + return ""; + } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java index 4fd1d361ba..740dd95e7d 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java @@ -13,48 +13,134 @@ 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.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; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.LOGOUT_ENDPOINT_SETTING; +import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OSO_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PASSWORD_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PROFILE_ENDPOINT_SETTING; import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING; 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.USE_NONCE_SETTING; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Maps; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.util.Collections; import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.eclipse.che.commons.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** @author Max Shaposhnik (mshaposh@redhat.com) */ @Singleton public class KeycloakSettings { + private static final Logger LOG = LoggerFactory.getLogger(KeycloakSettings.class); private final Map settings; @Inject public KeycloakSettings( - @Named(AUTH_SERVER_URL_SETTING) String serverURL, - @Named(REALM_SETTING) String realm, + @Nullable @Named(JS_ADAPTER_URL_SETTING) String jsAdapterUrl, + @Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL, + @Nullable @Named(REALM_SETTING) String realm, @Named(CLIENT_ID_SETTING) String clientId, + @Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProvider, + @Named(USE_NONCE_SETTING) boolean useNonce, @Nullable @Named(OSO_ENDPOINT_SETTING) String osoEndpoint, @Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint) { + + if (serverURL == null && oidcProvider == null) { + throw new RuntimeException( + "Either the '" + + AUTH_SERVER_URL_SETTING + + "' or '" + + OIDC_PROVIDER_SETTING + + "' property should be set"); + } + + if (oidcProvider == null && realm == null) { + throw new RuntimeException("The '" + REALM_SETTING + "' property should be set"); + } + + String wellKnownEndpoint = oidcProvider != null ? oidcProvider : serverURL + "/realms/" + realm; + if (!wellKnownEndpoint.endsWith("/")) { + wellKnownEndpoint = wellKnownEndpoint + "/"; + } + wellKnownEndpoint += ".well-known/openid-configuration"; + + LOG.info("Retrieving OpenId configuration from endpoint: {}", wellKnownEndpoint); + + URL url; + Map openIdConfiguration; + try { + url = new URL(wellKnownEndpoint); + final InputStream inputStream = url.openStream(); + final JsonFactory factory = new JsonFactory(); + final JsonParser parser = factory.createParser(inputStream); + final TypeReference> typeReference = + new TypeReference>() {}; + openIdConfiguration = new ObjectMapper().reader().readValue(parser, typeReference); + } catch (IOException e) { + throw new RuntimeException( + "Exception while retrieving OpenId configuration from endpoint: " + wellKnownEndpoint, e); + } + + LOG.info("openid configuration = {}", openIdConfiguration); + Map settings = Maps.newHashMap(); - settings.put(AUTH_SERVER_URL_SETTING, serverURL); settings.put(CLIENT_ID_SETTING, clientId); settings.put(REALM_SETTING, realm); - settings.put(PROFILE_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account"); - settings.put(PASSWORD_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account/password"); - settings.put( - LOGOUT_ENDPOINT_SETTING, - serverURL + "/realms/" + realm + "/protocol/openid-connect/logout"); - settings.put( - TOKEN_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/protocol/openid-connect/token"); + if (serverURL != null) { + settings.put(AUTH_SERVER_URL_SETTING, serverURL); + settings.put(PROFILE_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account"); + settings.put(PASSWORD_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account/password"); + settings.put( + LOGOUT_ENDPOINT_SETTING, + serverURL + "/realms/" + realm + "/protocol/openid-connect/logout"); + settings.put( + TOKEN_ENDPOINT_SETTING, + serverURL + "/realms/" + realm + "/protocol/openid-connect/token"); + } + String endSessionEndpoint = (String) openIdConfiguration.get("end_session_endpoint"); + if (endSessionEndpoint != null) { + settings.put(LOGOUT_ENDPOINT_SETTING, endSessionEndpoint); + } + String tokenEndpoint = (String) openIdConfiguration.get("token_endpoint"); + if (tokenEndpoint != null) { + settings.put(TOKEN_ENDPOINT_SETTING, tokenEndpoint); + } + String userInfoEndpoint = (String) openIdConfiguration.get("userinfo_endpoint"); + if (userInfoEndpoint != null) { + settings.put(USERINFO_ENDPOINT_SETTING, userInfoEndpoint); + } + String jwksUriEndpoint = (String) openIdConfiguration.get("jwks_uri"); + if (jwksUriEndpoint != null) { + settings.put(JWKS_ENDPOINT_SETTING, jwksUriEndpoint); + } settings.put(OSO_ENDPOINT_SETTING, osoEndpoint); settings.put(GITHUB_ENDPOINT_SETTING, gitHubEndpoint); + if (oidcProvider != null) { + settings.put(OIDC_PROVIDER_SETTING, oidcProvider); + } + settings.put(USE_NONCE_SETTING, Boolean.toString(useNonce)); + if (jsAdapterUrl == null) { + jsAdapterUrl = + (oidcProvider != null) ? "/api/keycloak/OIDCKeycloak.js" : serverURL + "/js/keycloak.js"; + } + settings.put(JS_ADAPTER_URL_SETTING, jsAdapterUrl); + this.settings = Collections.unmodifiableMap(settings); } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java index fd1cf69c3d..db5c73084d 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/dao/KeycloakProfileDao.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; -import javax.inject.Named; import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.NotFoundException; @@ -25,6 +24,7 @@ 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; @@ -43,12 +43,10 @@ public class KeycloakProfileDao implements ProfileDao { @Inject public KeycloakProfileDao( - @Named(KeycloakConstants.AUTH_SERVER_URL_SETTING) String authServerUrl, - @Named(KeycloakConstants.REALM_SETTING) String realm, - HttpJsonRequestFactory requestFactory) { + KeycloakSettings keycloakSettings, HttpJsonRequestFactory requestFactory) { this.requestFactory = requestFactory; this.keyclockCurrentUserInfoUrl = - authServerUrl + "/realms/" + realm + "/protocol/openid-connect/userinfo"; + keycloakSettings.get().get(KeycloakConstants.USERINFO_ENDPOINT_SETTING); } @Override diff --git a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java index 6a3d460a1e..4ce0985ba8 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java +++ b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java @@ -19,6 +19,9 @@ public class KeycloakConstants { public static final String AUTH_SERVER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "auth_server_url"; public static final String REALM_SETTING = KEYCLOAK_SETTING_PREFIX + "realm"; public static final String CLIENT_ID_SETTING = KEYCLOAK_SETTING_PREFIX + "client_id"; + public static final String OIDC_PROVIDER_SETTING = KEYCLOAK_SETTING_PREFIX + "oidc_provider"; + public static final String USE_NONCE_SETTING = KEYCLOAK_SETTING_PREFIX + "use_nonce"; + 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"; @@ -29,6 +32,9 @@ public class KeycloakConstants { KEYCLOAK_SETTING_PREFIX + "password.endpoint"; public static final String LOGOUT_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "logout.endpoint"; public static final String TOKEN_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "token.endpoint"; + public static final String JWKS_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "jwks.endpoint"; + public static final String USERINFO_ENDPOINT_SETTING = + KEYCLOAK_SETTING_PREFIX + "userinfo.endpoint"; public static final String GITHUB_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "github.endpoint"; public static String getEndpoint(String apiEndpoint) { diff --git a/workspace-loader/src/index.ts b/workspace-loader/src/index.ts index 02146bc92a..cec37cc47a 100644 --- a/workspace-loader/src/index.ts +++ b/workspace-loader/src/index.ts @@ -60,7 +60,7 @@ export class KeycloakLoader { script.type = 'text/javascript'; (script as any).language = 'javascript'; script.async = true; - script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js'; + script.src = keycloakSettings['che.keycloak.js_adapter_url']; script.onload = () => { resolve(this.initKeycloak(keycloakSettings)); @@ -79,16 +79,31 @@ export class KeycloakLoader { */ private initKeycloak(keycloakSettings: any): Promise { return new Promise((resolve, reject) => { - const keycloak = Keycloak({ - url: keycloakSettings['che.keycloak.auth_server_url'], - realm: keycloakSettings['che.keycloak.realm'], - clientId: keycloakSettings['che.keycloak.client_id'] - }); + function keycloakConfig() { + const theOidcProvider = keycloakSettings['che.keycloak.oidc_provider']; + if (!theOidcProvider) { + return { + url: keycloakSettings['che.keycloak.auth_server_url'], + realm: keycloakSettings['che.keycloak.realm'], + clientId: keycloakSettings['che.keycloak.client_id'] + }; + } else { + return { + oidcProvider: theOidcProvider, + clientId: keycloakSettings['che.keycloak.client_id'] + }; + } + } + const keycloak = Keycloak(keycloakConfig()); window['_keycloak'] = keycloak; + var useNonce; + if (typeof keycloakSettings['che.keycloak.use_nonce'] === 'string') { + useNonce = keycloakSettings['che.keycloak.use_nonce'].toLowerCase() === 'true'; + } keycloak - .init({onLoad: 'login-required', checkLoginIframe: false}) + .init({onLoad: 'login-required', checkLoginIframe: false, useNonce: useNonce}) .success(() => { resolve(keycloak); })