Skip to content

Commit

Permalink
Fix che-server when used external oidc provider. (eclipse-che#18563)
Browse files Browse the repository at this point in the history
* Fix che-server when used external oidc provider. Refactor code related to internal network. Add more tests, java docs.

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
  • Loading branch information
AndrienkoAleksandr authored Dec 16, 2020
1 parent 1a7bfa8 commit 7ee6ef3
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 7ee6ef3

Please sign in to comment.