diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java index 5fa23b01a9..b10c46e9fa 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java @@ -1,5 +1,8 @@ package org.keycloak.broker.provider.util; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -24,6 +27,9 @@ public class SimpleHttp { private Map headers; private Map params; + private SSLSocketFactory sslFactory; + private HostnameVerifier hostnameVerifier; + protected SimpleHttp(String url, String method) { this.url = url; this.method = method; @@ -53,6 +59,15 @@ public class SimpleHttp { return this; } + public SimpleHttp sslFactory(SSLSocketFactory factory) { + sslFactory = factory; + return this; + } + + public SimpleHttp hostnameVerifier(HostnameVerifier verifier) { + hostnameVerifier = verifier; + return this; + } public String asString() throws IOException { boolean get = method.equals("GET"); @@ -85,6 +100,7 @@ public class SimpleHttp { } HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + setupTruststoreIfApplicable(connection); OutputStream os = null; InputStream is = null; @@ -171,6 +187,7 @@ public class SimpleHttp { } HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + setupTruststoreIfApplicable(connection); OutputStream os = null; InputStream is = null; @@ -235,4 +252,13 @@ public class SimpleHttp { return writer.toString(); } + private void setupTruststoreIfApplicable(HttpURLConnection connection) { + if (connection instanceof HttpsURLConnection && sslFactory != null) { + HttpsURLConnection con = (HttpsURLConnection) connection; + con.setSSLSocketFactory(sslFactory); + if (hostnameVerifier != null) { + con.setHostnameVerifier(hostnameVerifier); + } + } + } } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 0c8dc3c8a2..6e2543bd5e 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -27,6 +27,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.connections.truststore.JSSETruststoreConfigurator; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -247,12 +248,15 @@ public abstract class AbstractOAuth2IdentityProviderorg.keycloak keycloak-model-api + + org.keycloak + keycloak-connections-truststore + org.apache.httpcomponents httpclient diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java index a06b296025..67f7cbd5d4 100755 --- a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType; import org.apache.http.impl.client.CloseableHttpClient; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.connections.truststore.TruststoreProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.common.util.EnvUtil; @@ -28,9 +29,12 @@ public class DefaultHttpClientFactory implements HttpClientFactory { private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class); private volatile CloseableHttpClient httpClient; + private Config.Scope config; @Override public HttpClientProvider create(KeycloakSession session) { + lazyInit(session); + return new HttpClientProvider() { @Override public HttpClient getHttpClient() { @@ -74,7 +78,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory { @Override public void close() { try { - httpClient.close(); + if (httpClient != null) { + httpClient.close(); + } } catch (IOException e) { } @@ -87,46 +93,62 @@ public class DefaultHttpClientFactory implements HttpClientFactory { @Override public void init(Config.Scope config) { - long socketTimeout = config.getLong("socket-timeout-millis", -1L); - long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); - int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0); - int connectionPoolSize = config.getInt("connection-pool-size", 200); - boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); - boolean disableCookies = config.getBoolean("disable-cookies", true); - String hostnameVerificationPolicy = config.get("hostname-verification-policy", "WILDCARD"); - HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = HttpClientBuilder.HostnameVerificationPolicy.valueOf(hostnameVerificationPolicy); - String truststore = config.get("truststore"); - String truststorePassword = config.get("truststore-password"); - String clientKeystore = config.get("client-keystore"); - String clientKeystorePassword = config.get("client-keystore-password"); - String clientPrivateKeyPassword = config.get("client-key-password"); + this.config = config; + } - HttpClientBuilder builder = new HttpClientBuilder(); - builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) - .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) - .maxPooledPerRoute(maxPooledPerRoute) - .connectionPoolSize(connectionPoolSize) - .hostnameVerification(hostnamePolicy) - .disableCookies(disableCookies); - if (disableTrustManager) builder.disableTrustManager(); - if (truststore != null) { - truststore = EnvUtil.replace(truststore); - try { - builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword)); - } catch (Exception e) { - throw new RuntimeException("Failed to load truststore", e); + private void lazyInit(KeycloakSession session) { + if (httpClient == null) { + synchronized(this) { + if (httpClient == null) { + long socketTimeout = config.getLong("socket-timeout-millis", -1L); + long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0); + int connectionPoolSize = config.getInt("connection-pool-size", 200); + boolean disableCookies = config.getBoolean("disable-cookies", true); + String clientKeystore = config.get("client-keystore"); + String clientKeystorePassword = config.get("client-keystore-password"); + String clientPrivateKeyPassword = config.get("client-key-password"); + + TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class); + boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null; + if (disableTrustManager) { + logger.warn("Truststore is disabled"); + } + HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = disableTrustManager ? null + : HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name()); + + HttpClientBuilder builder = new HttpClientBuilder(); + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .disableCookies(disableCookies); + + if (disableTrustManager) { + // TODO: is it ok to do away with disabling trust manager? + //builder.disableTrustManager(); + } else { + builder.hostnameVerification(hostnamePolicy); + try { + builder.trustStore(truststoreProvider.getTruststore()); + } catch (Exception e) { + throw new RuntimeException("Failed to load truststore", e); + } + } + + if (clientKeystore != null) { + clientKeystore = EnvUtil.replace(clientKeystore); + try { + KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); + builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load keystore", e); + } + } + httpClient = builder.build(); + } } } - if (clientKeystore != null) { - clientKeystore = EnvUtil.replace(clientKeystore); - try { - KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); - builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); - } catch (Exception e) { - throw new RuntimeException("Failed to load keystore", e); - } - } - httpClient = builder.build(); } @Override diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java index 06683822b1..a16c12ac84 100755 --- a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java @@ -145,7 +145,7 @@ public class HttpClientBuilder { * Disable cookie management. */ public HttpClientBuilder disableCookies(boolean disable) { - this.disableTrustManager = disable; + this.disableCookies = disable; return this; } diff --git a/connections/pom.xml b/connections/pom.xml index b931994b52..17cca7f33d 100755 --- a/connections/pom.xml +++ b/connections/pom.xml @@ -19,6 +19,7 @@ mongo mongo-update http-client + truststore diff --git a/connections/truststore/pom.xml b/connections/truststore/pom.xml new file mode 100755 index 0000000000..d72b6d809e --- /dev/null +++ b/connections/truststore/pom.xml @@ -0,0 +1,35 @@ + + + + keycloak-parent + org.keycloak + 1.8.0.CR1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-connections-truststore + Keycloak Truststore + + + + + org.apache.httpcomponents + httpclient + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-model-api + + + org.jboss.logging + jboss-logging + provided + + + diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java new file mode 100644 index 0000000000..d49ddb1c57 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java @@ -0,0 +1,31 @@ +package org.keycloak.connections.truststore; + +import java.security.KeyStore; + +/** + * @author Marko Strukelj + */ +public class FileTruststoreProvider implements TruststoreProvider { + + private final HostnameVerificationPolicy policy; + private final KeyStore truststore; + + FileTruststoreProvider(KeyStore truststore, HostnameVerificationPolicy policy) { + this.policy = policy; + this.truststore = truststore; + } + + @Override + public HostnameVerificationPolicy getPolicy() { + return policy; + } + + @Override + public KeyStore getTruststore() { + return truststore; + } + + @Override + public void close() { + } +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java new file mode 100644 index 0000000000..0e2a5d3f20 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java @@ -0,0 +1,102 @@ +package org.keycloak.connections.truststore; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; + +/** + * @author Marko Strukelj + */ +public class FileTruststoreProviderFactory implements TruststoreProviderFactory { + + private static final Logger log = Logger.getLogger(FileTruststoreProviderFactory.class); + + private TruststoreProvider provider; + + @Override + public TruststoreProvider create(KeycloakSession session) { + return provider; + } + + @Override + public void init(Config.Scope config) { + + String storepath = config.get("file"); + String pass = config.get("password"); + String policy = config.get("hostname-verification-policy"); + Boolean disabled = config.getBoolean("disabled", null); + + // if "truststore" . "file" is not configured then it is disabled + if (storepath == null && pass == null && policy == null && disabled == null) { + return; + } + + // if explicitly disabled + if (disabled != null && disabled) { + return; + } + + HostnameVerificationPolicy verificationPolicy = null; + KeyStore truststore = null; + + if (storepath == null) { + throw new RuntimeException("Attribute 'file' missing in 'truststore':'file' configuration"); + } + if (pass == null) { + throw new RuntimeException("Attribute 'password' missing in 'truststore':'file' configuration"); + } + + try { + truststore = loadStore(storepath, pass == null ? null :pass.toCharArray()); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize TruststoreProviderFactory: " + new File(storepath).getAbsolutePath(), e); + } + if (policy == null) { + verificationPolicy = HostnameVerificationPolicy.WILDCARD; + } else { + try { + verificationPolicy = HostnameVerificationPolicy.valueOf(policy); + } catch (Exception e) { + throw new RuntimeException("Invalid value for 'hostname-verification-policy': " + policy + " (must be one of: ANY, WILDCARD, STRICT)"); + } + } + + provider = new FileTruststoreProvider(truststore, verificationPolicy); + TruststoreProviderSingleton.set(provider); + log.debug("File trustore provider initialized: " + new File(storepath).getAbsolutePath()); + } + + private KeyStore loadStore(String path, char[] password) throws Exception { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream is = new FileInputStream(path); + try { + ks.load(is, password); + return ks; + } finally { + try { + is.close(); + } catch (IOException ignored) { + } + } + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "file"; + } +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java new file mode 100644 index 0000000000..0b6c53b1e4 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java @@ -0,0 +1,19 @@ +package org.keycloak.connections.truststore; + +public enum HostnameVerificationPolicy { + + /** + * Hostname verification is not done on the server's certificate + */ + ANY, + + /** + * Allows wildcards in subdomain names i.e. *.foo.com + */ + WILDCARD, + + /** + * CN must match hostname connecting to + */ + STRICT +} \ No newline at end of file diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java new file mode 100644 index 0000000000..e323f7af19 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java @@ -0,0 +1,106 @@ +package org.keycloak.connections.truststore; + +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +/** + * @author Marko Strukelj + */ +public class JSSETruststoreConfigurator { + + private TruststoreProvider provider; + private volatile javax.net.ssl.SSLSocketFactory sslFactory; + private volatile TrustManager[] tm; + + public JSSETruststoreConfigurator(KeycloakSession session) { + KeycloakSessionFactory factory = session.getKeycloakSessionFactory(); + TruststoreProviderFactory truststoreFactory = (TruststoreProviderFactory) factory.getProviderFactory(TruststoreProvider.class, "file"); + + provider = truststoreFactory.create(session); + if (provider != null && provider.getTruststore() == null) { + provider = null; + } + } + + public JSSETruststoreConfigurator(TruststoreProvider provider) { + this.provider = provider; + } + + public javax.net.ssl.SSLSocketFactory getSSLSocketFactory() { + if (provider == null) { + return null; + } + + if (sslFactory == null) { + synchronized(this) { + if (sslFactory == null) { + try { + SSLContext sslctx = SSLContext.getInstance("TLS"); + sslctx.init(null, getTrustManagers(), null); + sslFactory = sslctx.getSocketFactory(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize SSLContext: ", e); + } + } + } + } + return sslFactory; + } + + public TrustManager[] getTrustManagers() { + if (provider == null) { + return null; + } + + if (tm == null) { + synchronized (this) { + if (tm == null) { + TrustManagerFactory tmf = null; + try { + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(provider.getTruststore()); + tm = tmf.getTrustManagers(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize TrustManager: ", e); + } + } + } + } + return tm; + } + + public HostnameVerifier getHostnameVerifier() { + if (provider == null) { + return null; + } + + HostnameVerificationPolicy policy = provider.getPolicy(); + switch (policy) { + case ANY: + return new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + case WILDCARD: + return new BrowserCompatHostnameVerifier(); + case STRICT: + return new StrictHostnameVerifier(); + default: + throw new IllegalStateException("Unknown policy: " + policy.name()); + } + } + + public TruststoreProvider getProvider() { + return provider; + } +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java new file mode 100644 index 0000000000..86f56671e3 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java @@ -0,0 +1,87 @@ +package org.keycloak.connections.truststore; + +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + + +/** + * Using this class is ugly, but it is the only way to push our truststore to the default LDAP client implementation. + *

+ * This SSLSocketFactory can only use truststore configured by TruststoreProvider after the ProviderFactory was + * initialized using standard Spi load / init mechanism. That will only happen if "truststore" provider is configured + * in keycloak-server.json. + *

+ * If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault(). + * + * @author Marko Strukelj + */ + +public class SSLSocketFactory extends javax.net.ssl.SSLSocketFactory { + + private static final Logger log = Logger.getLogger(SSLSocketFactory.class); + + private static SSLSocketFactory instance; + + private final javax.net.ssl.SSLSocketFactory sslsf; + + private SSLSocketFactory() { + + TruststoreProvider provider = TruststoreProviderSingleton.get(); + javax.net.ssl.SSLSocketFactory sf = null; + if (provider != null) { + sf = new JSSETruststoreConfigurator(provider).getSSLSocketFactory(); + } + + if (sf == null) { + log.info("No truststore provider found - using default SSLSocketFactory"); + sf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(); + } + + sslsf = sf; + } + + public static synchronized SSLSocketFactory getDefault() { + if (instance == null) { + instance = new SSLSocketFactory(); + } + return instance; + } + + @Override + public String[] getDefaultCipherSuites() { + return sslsf.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslsf.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + return sslsf.createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return sslsf.createSocket(host, port); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return sslsf.createSocket(host, port, localHost, localPort); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return sslsf.createSocket(host, port); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return sslsf.createSocket(address, port, localAddress, localPort); + } +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java new file mode 100644 index 0000000000..54cc6c3207 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java @@ -0,0 +1,15 @@ +package org.keycloak.connections.truststore; + +import org.keycloak.provider.Provider; + +import java.security.KeyStore; + +/** + * @author Marko Strukelj + */ +public interface TruststoreProvider extends Provider { + + HostnameVerificationPolicy getPolicy(); + + KeyStore getTruststore(); +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java new file mode 100644 index 0000000000..10ed86750f --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.connections.truststore; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marko Strukelj + */ +public interface TruststoreProviderFactory extends ProviderFactory { +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java new file mode 100644 index 0000000000..520e7814d1 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java @@ -0,0 +1,17 @@ +package org.keycloak.connections.truststore; + +/** + * @author Marko Strukelj + */ +class TruststoreProviderSingleton { + + static private TruststoreProvider provider; + + static void set(TruststoreProvider tp) { + provider = tp; + } + + static TruststoreProvider get() { + return provider; + } +} diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java new file mode 100644 index 0000000000..385346e600 --- /dev/null +++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java @@ -0,0 +1,31 @@ +package org.keycloak.connections.truststore; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marko Strukelj + */ +public class TruststoreSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "truststore"; + } + + @Override + public Class getProviderClass() { + return TruststoreProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return TruststoreProviderFactory.class; + } +} diff --git a/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory new file mode 100644 index 0000000000..5b38b43c63 --- /dev/null +++ b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory @@ -0,0 +1 @@ +org.keycloak.connections.truststore.FileTruststoreProviderFactory \ No newline at end of file diff --git a/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..3be9970d46 --- /dev/null +++ b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.connections.truststore.TruststoreSpi \ No newline at end of file diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml index 019e73c518..422cc2852a 100755 --- a/dependencies/server-min/pom.xml +++ b/dependencies/server-min/pom.xml @@ -131,7 +131,10 @@ org.keycloak keycloak-export-import-single-file - + + org.keycloak + keycloak-connections-truststore + org.keycloak keycloak-connections-http-client diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json index 3e4315c136..a5b4d1b827 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json @@ -45,9 +45,7 @@ }, "connectionsHttpClient": { - "default": { - "disable-trust-manager": true - } + "default": {} }, "connectionsJpa": { diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml index b7ed82e65e..e7e18c26ba 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml @@ -13,6 +13,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml index 2f4a97da0b..d070fa9859 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml @@ -13,6 +13,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml index 27295ddf15..5611eca9bf 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml @@ -16,6 +16,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml new file mode 100755 index 0000000000..8599425107 --- /dev/null +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 7e61fb4bd6..0a67753f3f 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -8,6 +8,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml index 927b03fdf4..12d9ebb728 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml @@ -18,6 +18,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/build.xml b/distribution/server-overlay/eap6/eap6-server-modules/build.xml index 09be9a4642..97cbb6f5de 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/build.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/build.xml @@ -181,6 +181,10 @@ + + + + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml index fac17da2f9..6cb957f510 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml @@ -13,6 +13,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml index f2155cb666..b8724689fd 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml @@ -13,6 +13,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml index 60489b2cfc..a10b80f70a 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml @@ -13,6 +13,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml new file mode 100755 index 0000000000..5c449760f0 --- /dev/null +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml index 7e61fb4bd6..0a67753f3f 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml @@ -8,6 +8,7 @@ + diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index 56f2537006..893c5e2a7d 100755 --- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -18,6 +18,7 @@ + diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index dc7a70c0c0..369d5f1b30 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -382,9 +382,7 @@ bin/add-user.[sh|bat] -r master -u -p By default the setting is like this: Possible configuration options are: @@ -421,15 +419,6 @@ bin/add-user.[sh|bat] -r master -u -p - - disable-trust-manager - - - If true, HTTPS server certificates are not verified. If you set this to false, you must - configure a truststore. - - - disable-cookies @@ -439,44 +428,6 @@ bin/add-user.[sh|bat] -r master -u -p - - hostname-verification-policy - - - WILDCARD by default. For HTTPS requests, this verifies the hostname - of the server's certificate. ANY means that the hostname is not verified. - WILDCARD Allows wildcards in subdomain names i.e. *.foo.com. - STRICT CN must match hostname exactly. - - - - - truststore - - - The value is the file path to a Java keystore file. If - you prefix the path with classpath:, then the truststore will be obtained - from the deployment's classpath instead. - HTTPS - requests need a way to verify the host of the server they are talking to. This is - what the trustore does. The keystore contains one or more trusted - host certificates or certificate authorities. - - - - - truststore-password - - - Password for the truststore keystore. - This is - REQUIRED - if - truststore - is set. - - - client-keystore @@ -516,13 +467,111 @@ bin/add-user.[sh|bat] -r master -u -p +

+ Securing Outgoing Server HTTP Requests + + When Keycloak connects out to remote HTTP endpoints over secure https connection, it has to validate the other + server's certificate in order to ensure it is connecting to a trusted server. That is necessary in order to + prevent man-in-the-middle attacks. + + + How certificates are validated is configured in the standalone/configuration/keycloak-server.json. + By default truststore provider is not configured, and any https connections fall back to standard java truststore + configuration as described in + Java's JSSE Reference Guide - using javax.net.ssl.trustStore system property, + otherwise cacerts file that comes with java is used. + + + Truststore is used when connecting securely to identity brokers, LDAP identity providers, when sending emails, + and for backchannel communication with client applications. + + Some of these facilities may - in case when no trusted certificate is found in your configured truststore - + fallback to using the JSSE provided truststore. + + The default JavaMail API implementation used to send out emails behaves in this way, for example. + + + You can add your truststore configuration by using the following template: + + + + + + Possible configuration options are: + + + + file + + + The value is the file path to a Java keystore file. + HTTPS requests need a way to verify the host of the server they are talking to. This is + what the trustore does. The keystore contains one or more trusted host certificates or + certificate authorities. Truststore file should only contain public certificates of your secured hosts. + + This is + REQUIRED + if disabled is not true. + + + + + password + + + Password for the truststore. + This is + REQUIRED + if disabled is not true. + + + + + hostname-verification-policy + + + WILDCARD by default. For HTTPS requests, this verifies the hostname + of the server's certificate. ANY means that the hostname is not verified. + WILDCARD Allows wildcards in subdomain names i.e. *.foo.com. + STRICT CN must match hostname exactly. + + + + + disabled + + + If true (default value), truststore configuration will be ignored, and certificate checking will + fall back to JSSE configuration as described. If set to false, you must + configure file, and password for the truststore. + + + + + + + You can use keytool to create a new truststore file and add trusted host certificates to it: + + +$ keytool -import -alias HOSTDOMAIN -keystore truststore.jks -file host-certificate.cer + + +
SSL/HTTPS Requirement/Modes - Keycloak is not set up by default to handle SSL/HTTPS in either the - war distribution or appliance. It is highly recommended that you either enable SSL on the Keycloak server - itself or on a reverse proxy in front of the Keycloak server. + Keycloak is not set up by default to handle SSL/HTTPS. It is highly recommended that you either enable SSL + on the Keycloak server itself or on a reverse proxy in front of the Keycloak server. diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java index 3d0d22bd76..612fd7343c 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java @@ -467,6 +467,9 @@ public class LDAPOperationManager { if (protocol != null) { env.put(Context.SECURITY_PROTOCOL, protocol); + if ("ssl".equals(protocol)) { + env.put("java.naming.ldap.factory.socket", "org.keycloak.connections.truststore.SSLSocketFactory"); + } } String bindDN = this.config.getBindDN(); diff --git a/pom.xml b/pom.xml index 03e1416a39..83ac183320 100755 --- a/pom.xml +++ b/pom.xml @@ -640,6 +640,11 @@ keycloak-connections-mongo-update ${project.version} + + org.keycloak + keycloak-connections-truststore + ${project.version} + org.keycloak keycloak-common diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java index 1a06877867..5f7f68bb9e 100644 --- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java +++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java @@ -1,6 +1,9 @@ package org.keycloak.email; import org.jboss.logging.Logger; +import org.keycloak.connections.truststore.HostnameVerificationPolicy; +import org.keycloak.connections.truststore.JSSETruststoreConfigurator; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -12,6 +15,8 @@ import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Map; import java.util.Properties; @@ -23,6 +28,12 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class); + private final KeycloakSession session; + + public DefaultEmailSenderProvider(KeycloakSession session) { + this.session = session; + } + @Override public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { try { @@ -52,6 +63,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { props.setProperty("mail.smtp.starttls.enable", "true"); } + if (ssl || starttls) { + setupTruststore(props); + } + props.setProperty("mail.smtp.timeout", "10000"); props.setProperty("mail.smtp.connectiontimeout", "10000"); @@ -94,9 +109,18 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider { } } + private void setupTruststore(Properties props) throws NoSuchAlgorithmException, KeyManagementException { + + JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session); + + props.put("mail.smtp.ssl.socketFactory", configurator.getSSLSocketFactory()); + if (configurator.getProvider().getPolicy() == HostnameVerificationPolicy.ANY) { + props.setProperty("mail.smtp.ssl.trust", "*"); + } + } + @Override public void close() { } - } diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java index 96770001ff..de8dfd7d7c 100644 --- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java +++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java @@ -11,7 +11,7 @@ public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFac @Override public EmailSenderProvider create(KeycloakSession session) { - return new DefaultEmailSenderProvider(); + return new DefaultEmailSenderProvider(session); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 4d4c2f6957..09782319bc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -72,9 +72,7 @@ }, "connectionsHttpClient": { - "default": { - "disable-trust-manager": true - } + "default": {} }, @@ -99,5 +97,14 @@ "databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}", "connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}" } + }, + + "truststore": { + "file": { + "file": "${keycloak.truststore.file:src/main/keystore/keycloak.truststore}", + "password": "${keycloak.truststore.password:secret}", + "hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}", + "disabled": "${keycloak.truststore.disabled:true}" + } } -} \ No newline at end of file +} diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 26fec935b5..41c2cf5fa2 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -51,9 +51,7 @@ }, "connectionsHttpClient": { - "default": { - "disable-trust-manager": true - } + "default": {} }, diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java index 4abe94dd1e..cdc919e906 100644 --- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java +++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java @@ -43,6 +43,9 @@ public class LDAPEmbeddedServer { private static final String DEFAULT_BIND_HOST = "localhost"; private static final String DEFAULT_BIND_PORT = "10389"; private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif"; + private static final String PROPERTY_ENABLE_SSL = "enableSSL"; + private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile"; + private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword"; public static final String DSF_INMEMORY = "mem"; public static final String DSF_FILE = "file"; @@ -56,6 +59,9 @@ public class LDAPEmbeddedServer { protected String ldifFile; protected String ldapSaslPrincipal; protected String directoryServiceFactory; + protected boolean enableSSL = false; + protected String keystoreFile; + protected String certPassword; protected DirectoryService directoryService; protected LdapServer ldapServer; @@ -97,6 +103,9 @@ public class LDAPEmbeddedServer { this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE); this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null); this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF); + this.enableSSL = Boolean.valueOf(readProperty(PROPERTY_ENABLE_SSL, "false")); + this.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null); + this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null); } protected String readProperty(String propertyName, String defaultValue) { @@ -194,6 +203,11 @@ public class LDAPEmbeddedServer { // Read the transports Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); + if (enableSSL) { + ldap.setEnableSSL(true); + ldapServer.setKeystoreFile(keystoreFile); + ldapServer.setCertificatePassword(certPassword); + } ldapServer.addTransports( ldap ); // Associate the DS to this LdapServer