Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix failing Che when used external OIDC provider. Port to 7.23.x #18631

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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