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 <dfestal@redhat.com>
6.19.x
David Festal 2018-03-23 14:44:23 +01:00 committed by GitHub
parent d8d24aece6
commit ff3459d2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1737 additions and 128 deletions

View File

@ -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 `<che-server>/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

View File

@ -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<T> {
(value: T | PromiseLike<T>): Promise<T>;
@ -64,18 +72,18 @@ function keycloakLoad(keycloakSettings: any) {
return new Promise((resolve: IResolveFn<any>, reject: IRejectFn<any>) => {
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<any>, reject: IRejectFn<any>) => {
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;

View File

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

View File

@ -52,6 +52,7 @@
<che-label-container che-label-name="">
<che-button-default class="prifile-add-button"
che-button-title="Edit" name="editButton"
ng-click="profileController.editProfile()"></che-button-default>
ng-click="profileController.editProfile()"
ng-disabled="profileController.cannotEdit"></che-button-default>
</che-label-container>
</md-content>

View File

@ -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 `<che-server>/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
########################################################################################
##### #####

View File

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

View File

@ -39,17 +39,30 @@ public final class Keycloak extends JavaScriptObject {
}-*/;
public static native Promise<Keycloak> 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);
})

View File

@ -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<Void, Throwable>() {
@Override
public void makeCall(final Callback<Void, Throwable> callback) {
ScriptInjector.fromUrl(
settings.get(AUTH_SERVER_URL_SETTING) + "/js/keycloak.js")
ScriptInjector.fromUrl(jsAdapterUrl)
.setCallback(
new Callback<Void, Exception>() {
@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);
}

View File

@ -25,6 +25,18 @@
<dto-generator-out-directory>${project.build.directory}/generated-sources/dto/</dto-generator-out-directory>
</properties>
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>

View File

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

View File

@ -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<Map<String, String>>() {}.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<String, String> 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 {

View File

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

View File

@ -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<String, String> 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<String, Object> openIdConfiguration;
try {
url = new URL(wellKnownEndpoint);
final InputStream inputStream = url.openStream();
final JsonFactory factory = new JsonFactory();
final JsonParser parser = factory.createParser(inputStream);
final TypeReference<Map<String, Object>> typeReference =
new TypeReference<Map<String, Object>>() {};
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<String, String> 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);
}

View File

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

View File

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

View File

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