Skip to content

Commit

Permalink
Fix failing Che when used external OIDC provider. (#18631)
Browse files Browse the repository at this point in the history
Fix failing Che when used external OIDC provider. Refactor code related to internal network. Add more tests and java docs.

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
  • Loading branch information
AndrienkoAleksandr authored Dec 16, 2020
1 parent 6776344 commit 2d8d76b
Show file tree
Hide file tree
Showing 13 changed files with 780 additions and 130 deletions.
5 changes: 5 additions & 0 deletions multiuser/keycloak/che-multiuser-keycloak-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;

/** Constructs {@link UrlJwkProvider} based on Jwk endpoint from keycloak settings */
public class KeycloakJwkProvider implements Provider<JwkProvider> {

private final JwkProvider jwkProvider;

@Inject
public KeycloakJwkProvider(KeycloakSettings keycloakSettings) throws MalformedURLException {

final String jwksUrl =
keycloakSettings.getInternalSettings().get(KeycloakConstants.JWKS_ENDPOINT_SETTING);
public KeycloakJwkProvider(OIDCInfo oidcInfo) throws MalformedURLException {
final String jwksUrl = oidcInfo.getJwksUri();

if (jwksUrl == null) {
throw new ConfigurationException("Jwks endpoint url not found in keycloak settings");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.eclipse.che.api.core.ApiException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -36,11 +35,9 @@ public class KeycloakProfileRetriever {
private final HttpJsonRequestFactory requestFactory;

@Inject
public KeycloakProfileRetriever(
KeycloakSettings keycloakSettings, HttpJsonRequestFactory requestFactory) {
public KeycloakProfileRetriever(OIDCInfo oidcInfo, HttpJsonRequestFactory requestFactory) {
this.requestFactory = requestFactory;
this.keyclockCurrentUserInfoUrl =
keycloakSettings.getInternalSettings().get(KeycloakConstants.USERINFO_ENDPOINT_SETTING);
this.keyclockCurrentUserInfoUrl = oidcInfo.getUserInfoEndpoint();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/
package org.eclipse.che.multiuser.keycloak.server;

import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_INTERNAL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;

import com.google.common.io.CharStreams;
Expand Down Expand Up @@ -62,6 +61,7 @@
public class KeycloakServiceClient {

private KeycloakSettings keycloakSettings;
private final OIDCInfo oidcInfo;

private static final Pattern assotiateUserPattern =
Pattern.compile("User (.+) is not associated with identity provider (.+)");
Expand All @@ -70,8 +70,10 @@ public class KeycloakServiceClient {
private JwtParser jwtParser;

@Inject
public KeycloakServiceClient(KeycloakSettings keycloakSettings, JwtParser jwtParser) {
public KeycloakServiceClient(
KeycloakSettings keycloakSettings, OIDCInfo oidcInfo, JwtParser jwtParser) {
this.keycloakSettings = keycloakSettings;
this.oidcInfo = oidcInfo;
this.jwtParser = jwtParser;
}

Expand Down Expand Up @@ -101,8 +103,7 @@ public String getAccountLinkingURL(
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
final String hash = Base64.getUrlEncoder().encodeToString(check);

return UriBuilder.fromUri(
keycloakSettings.getInternalSettings().get(AUTH_SERVER_URL_INTERNAL_SETTING))
return UriBuilder.fromUri(oidcInfo.getAuthServerURL())
.path("/realms/{realm}/broker/{provider}/link")
.queryParam("nonce", nonce)
.queryParam("hash", hash)
Expand All @@ -128,8 +129,7 @@ public KeycloakTokenResponse getIdentityProviderToken(String oauthProvider)
throws ForbiddenException, BadRequestException, IOException, NotFoundException,
ServerException, UnauthorizedException {
String url =
UriBuilder.fromUri(
keycloakSettings.getInternalSettings().get(AUTH_SERVER_URL_INTERNAL_SETTING))
UriBuilder.fromUri(oidcInfo.getAuthServerURL())
.path("/realms/{realm}/broker/{provider}/token")
.build(keycloakSettings.get().get(REALM_SETTING), oauthProvider)
.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
*/
package org.eclipse.che.multiuser.keycloak.server;

import static com.google.common.base.MoreObjects.firstNonNull;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_INTERNAL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_DASHBOARD;
Expand All @@ -32,112 +30,44 @@
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_FIXED_REDIRECT_URLS_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING;

import com.fasterxml.jackson.core.JsonFactory;
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.eclipse.che.commons.proxy.ProxyAuthenticator;
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 static final String DEFAULT_USERNAME_CLAIM = "preferred_username";
protected static final String DEFAULT_USERNAME_CLAIM = "preferred_username";

/**
* Public Keycloak connection settings. It contains information about keycloak api urls and
* information required to make Keycloak connection using public domain hostname. This info will
* be shared with frontend.
*/
private final Map<String, String> settings;
/**
* Internal network Keycloak connection settings. It contains information about keycloak api urls
* and information required to make connection using k8s/openshift internal services hostname.
* This info will be used only on the Che server side. If using internal network is disabled, then
* will be included settings with public domain hostname.
*/
private final Map<String, String> internalSettings;
private final String oidcProviderUrl;

@Inject
public KeycloakSettings(
@Named("che.api") String cheServerEndpoint,
@Nullable @Named(JS_ADAPTER_URL_SETTING) String jsAdapterUrl,
@Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL,
@Nullable @Named(AUTH_SERVER_URL_INTERNAL_SETTING) String serverInternalURL,
@Nullable @Named(REALM_SETTING) String realm,
@Named(CLIENT_ID_SETTING) String clientId,
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProvider,
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProviderUrl,
@Nullable @Named(USERNAME_CLAIM_SETTING) String usernameClaim,
@Named(USE_NONCE_SETTING) boolean useNonce,
@Nullable @Named(OSO_ENDPOINT_SETTING) String osoEndpoint,
@Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint,
@Named(USE_FIXED_REDIRECT_URLS_SETTING) boolean useFixedRedirectUrls) {

serverInternalURL = (serverInternalURL != null) ? serverInternalURL : serverURL;

if (serverURL == null && serverInternalURL == null && oidcProvider == null) {
throw new RuntimeException(
"Either the '"
+ AUTH_SERVER_URL_SETTING
+ "'or'"
+ AUTH_SERVER_URL_INTERNAL_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 = firstNonNull(oidcProvider, serverInternalURL + "/realms/" + realm);
if (!wellKnownEndpoint.endsWith("/")) {
wellKnownEndpoint = wellKnownEndpoint + "/";
}
wellKnownEndpoint += ".well-known/openid-configuration";

LOG.info("Retrieving OpenId configuration from endpoint: {}", wellKnownEndpoint);

Map<String, Object> openIdConfiguration;
ProxyAuthenticator.initAuthenticator(wellKnownEndpoint);
try (InputStream inputStream = new URL(wellKnownEndpoint).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);
} finally {
ProxyAuthenticator.resetAuthenticator();
}

LOG.info("openid configuration = {}", openIdConfiguration);
@Named(USE_FIXED_REDIRECT_URLS_SETTING) boolean useFixedRedirectUrls,
OIDCInfo oidcInfo) {
this.oidcProviderUrl = oidcProviderUrl;

Map<String, String> settings = Maps.newHashMap();
Map<String, String> internalSettings = Maps.newHashMap();
settings.put(
USERNAME_CLAIM_SETTING, usernameClaim == null ? DEFAULT_USERNAME_CLAIM : usernameClaim);
settings.put(CLIENT_ID_SETTING, clientId);
settings.put(REALM_SETTING, realm);

if (serverInternalURL != null) {
internalSettings.put(AUTH_SERVER_URL_INTERNAL_SETTING, serverInternalURL);
}

if (serverURL != null) {
settings.put(AUTH_SERVER_URL_SETTING, serverURL);
settings.put(PROFILE_ENDPOINT_SETTING, serverURL + "/realms/" + realm + "/account");
Expand All @@ -149,37 +79,38 @@ public KeycloakSettings(
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);

if (oidcInfo.getEndSessionPublicEndpoint() != null) {
settings.put(LOGOUT_ENDPOINT_SETTING, oidcInfo.getEndSessionPublicEndpoint());
}
String tokenEndpoint = (String) openIdConfiguration.get("token_endpoint");
if (tokenEndpoint != null) {
settings.put(TOKEN_ENDPOINT_SETTING, tokenEndpoint);
if (oidcInfo.getTokenPublicEndpoint() != null) {
settings.put(TOKEN_ENDPOINT_SETTING, oidcInfo.getTokenPublicEndpoint());
}

String userInfoEndpoint = (String) openIdConfiguration.get("userinfo_endpoint");
if (userInfoEndpoint != null) {
settings.put(USERINFO_ENDPOINT_SETTING, userInfoEndpoint);
if (serverURL != null) {
String internalInfoEndpoint = userInfoEndpoint.replace(serverURL, serverInternalURL);
internalSettings.put(USERINFO_ENDPOINT_SETTING, internalInfoEndpoint);
}
if (oidcInfo.getUserInfoPublicEndpoint() != null) {
settings.put(USERINFO_ENDPOINT_SETTING, oidcInfo.getUserInfoPublicEndpoint());
}
String jwksUriEndpoint = (String) openIdConfiguration.get("jwks_uri");
if (jwksUriEndpoint != null) {
settings.put(JWKS_ENDPOINT_SETTING, jwksUriEndpoint);
if (serverURL != null) {
String internalJwksUriEndpoint = jwksUriEndpoint.replace(serverURL, serverInternalURL);
internalSettings.put(JWKS_ENDPOINT_SETTING, internalJwksUriEndpoint);
}
if (oidcInfo.getJwksPublicUri() != null) {
settings.put(JWKS_ENDPOINT_SETTING, oidcInfo.getJwksPublicUri());
}

settings.put(OSO_ENDPOINT_SETTING, osoEndpoint);
settings.put(GITHUB_ENDPOINT_SETTING, gitHubEndpoint);

if (oidcProvider != null) {
settings.put(OIDC_PROVIDER_SETTING, oidcProvider);
this.setUpKeycloakJSAdaptersURLS(
settings, useNonce, useFixedRedirectUrls, jsAdapterUrl, cheServerEndpoint, serverURL);

this.settings = Collections.unmodifiableMap(settings);
}

private void setUpKeycloakJSAdaptersURLS(
Map<String, String> settings,
boolean useNonce,
boolean useFixedRedirectUrls,
String jsAdapterUrl,
String cheServerEndpoint,
String serverURL) {
if (oidcProviderUrl != null) {
settings.put(OIDC_PROVIDER_SETTING, oidcProviderUrl);
if (useFixedRedirectUrls) {
String rootUrl =
cheServerEndpoint.endsWith("/") ? cheServerEndpoint : cheServerEndpoint + "/";
Expand All @@ -188,22 +119,24 @@ public KeycloakSettings(
settings.put(FIXED_REDIRECT_URL_FOR_IDE, rootUrl + "keycloak/oidcCallbackIde.html");
}
}

settings.put(USE_NONCE_SETTING, Boolean.toString(useNonce));

if (jsAdapterUrl == null) {
jsAdapterUrl =
(oidcProvider != null) ? "/api/keycloak/OIDCKeycloak.js" : serverURL + "/js/keycloak.js";
(oidcProviderUrl != null)
? "/api/keycloak/OIDCKeycloak.js"
: serverURL + "/js/keycloak.js";
}
settings.put(JS_ADAPTER_URL_SETTING, jsAdapterUrl);

this.settings = Collections.unmodifiableMap(settings);
this.internalSettings = Collections.unmodifiableMap(internalSettings);
}

/**
* Public Keycloak connection settings. It contains information about keycloak api urls and
* information required to make Keycloak connection using public domain hostname. This info will
* be shared with frontend.
*/
public Map<String, String> get() {
return settings;
}

public Map<String, String> getInternalSettings() {
return internalSettings;
}
}
Loading

0 comments on commit 2d8d76b

Please sign in to comment.