Merge pull request #2000 from mstruk/truststore

KEYCLOAK-1717 Truststore SPI and file provider
This commit is contained in:
Stian Thorgersen 2016-01-11 09:24:53 +01:00
commit a6c852603e
41 changed files with 766 additions and 106 deletions

View file

@ -1,5 +1,8 @@
package org.keycloak.broker.provider.util; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -24,6 +27,9 @@ public class SimpleHttp {
private Map<String, String> headers; private Map<String, String> headers;
private Map<String, String> params; private Map<String, String> params;
private SSLSocketFactory sslFactory;
private HostnameVerifier hostnameVerifier;
protected SimpleHttp(String url, String method) { protected SimpleHttp(String url, String method) {
this.url = url; this.url = url;
this.method = method; this.method = method;
@ -53,6 +59,15 @@ public class SimpleHttp {
return this; 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 { public String asString() throws IOException {
boolean get = method.equals("GET"); boolean get = method.equals("GET");
@ -85,6 +100,7 @@ public class SimpleHttp {
} }
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
setupTruststoreIfApplicable(connection);
OutputStream os = null; OutputStream os = null;
InputStream is = null; InputStream is = null;
@ -171,6 +187,7 @@ public class SimpleHttp {
} }
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
setupTruststoreIfApplicable(connection);
OutputStream os = null; OutputStream os = null;
InputStream is = null; InputStream is = null;
@ -235,4 +252,13 @@ public class SimpleHttp {
return writer.toString(); 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);
}
}
}
} }

View file

@ -27,6 +27,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -247,12 +248,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
} }
public SimpleHttp generateTokenRequest(String authorizationCode) { public SimpleHttp generateTokenRequest(String authorizationCode) {
JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
return SimpleHttp.doPost(getConfig().getTokenUrl()) return SimpleHttp.doPost(getConfig().getTokenUrl())
.param(OAUTH2_PARAMETER_CODE, authorizationCode) .param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()) .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()) .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString()) .param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE); .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE)
.sslFactory(configurator.getSSLSocketFactory())
.hostnameVerifier(configurator.getHostnameVerifier());
} }
} }
} }

View file

@ -22,6 +22,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId> <artifactId>keycloak-model-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>

View file

@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.connections.truststore.TruststoreProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.common.util.EnvUtil; 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 static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private volatile CloseableHttpClient httpClient; private volatile CloseableHttpClient httpClient;
private Config.Scope config;
@Override @Override
public HttpClientProvider create(KeycloakSession session) { public HttpClientProvider create(KeycloakSession session) {
lazyInit(session);
return new HttpClientProvider() { return new HttpClientProvider() {
@Override @Override
public HttpClient getHttpClient() { public HttpClient getHttpClient() {
@ -74,7 +78,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override @Override
public void close() { public void close() {
try { try {
if (httpClient != null) {
httpClient.close(); httpClient.close();
}
} catch (IOException e) { } catch (IOException e) {
} }
@ -87,36 +93,49 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
this.config = config;
}
private void lazyInit(KeycloakSession session) {
if (httpClient == null) {
synchronized(this) {
if (httpClient == null) {
long socketTimeout = config.getLong("socket-timeout-millis", -1L); long socketTimeout = config.getLong("socket-timeout-millis", -1L);
long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0); int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
int connectionPoolSize = config.getInt("connection-pool-size", 200); int connectionPoolSize = config.getInt("connection-pool-size", 200);
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
boolean disableCookies = config.getBoolean("disable-cookies", true); 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 clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password"); String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-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(); HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute) .maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize) .connectionPoolSize(connectionPoolSize)
.hostnameVerification(hostnamePolicy)
.disableCookies(disableCookies); .disableCookies(disableCookies);
if (disableTrustManager) builder.disableTrustManager();
if (truststore != null) { if (disableTrustManager) {
truststore = EnvUtil.replace(truststore); // TODO: is it ok to do away with disabling trust manager?
//builder.disableTrustManager();
} else {
builder.hostnameVerification(hostnamePolicy);
try { try {
builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword)); builder.trustStore(truststoreProvider.getTruststore());
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to load truststore", e); throw new RuntimeException("Failed to load truststore", e);
} }
} }
if (clientKeystore != null) { if (clientKeystore != null) {
clientKeystore = EnvUtil.replace(clientKeystore); clientKeystore = EnvUtil.replace(clientKeystore);
try { try {
@ -128,6 +147,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
} }
httpClient = builder.build(); httpClient = builder.build();
} }
}
}
}
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {

View file

@ -145,7 +145,7 @@ public class HttpClientBuilder {
* Disable cookie management. * Disable cookie management.
*/ */
public HttpClientBuilder disableCookies(boolean disable) { public HttpClientBuilder disableCookies(boolean disable) {
this.disableTrustManager = disable; this.disableCookies = disable;
return this; return this;
} }

View file

@ -19,6 +19,7 @@
<module>mongo</module> <module>mongo</module>
<module>mongo-update</module> <module>mongo-update</module>
<module>http-client</module> <module>http-client</module>
<module>truststore</module>
</modules> </modules>
<build> <build>

35
connections/truststore/pom.xml Executable file
View file

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.8.0.CR1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-connections-truststore</artifactId>
<name>Keycloak Truststore</name>
<description/>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,31 @@
package org.keycloak.connections.truststore;
import java.security.KeyStore;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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() {
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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";
}
}

View file

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

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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;
}
}

View file

@ -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.
* <p>
* 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.
* <p>
* If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault().
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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);
}
}

View file

@ -0,0 +1,15 @@
package org.keycloak.connections.truststore;
import org.keycloak.provider.Provider;
import java.security.KeyStore;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface TruststoreProvider extends Provider {
HostnameVerificationPolicy getPolicy();
KeyStore getTruststore();
}

View file

@ -0,0 +1,9 @@
package org.keycloak.connections.truststore;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public interface TruststoreProviderFactory extends ProviderFactory<TruststoreProvider> {
}

View file

@ -0,0 +1,17 @@
package org.keycloak.connections.truststore;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
class TruststoreProviderSingleton {
static private TruststoreProvider provider;
static void set(TruststoreProvider tp) {
provider = tp;
}
static TruststoreProvider get() {
return provider;
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class TruststoreSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "truststore";
}
@Override
public Class<? extends Provider> getProviderClass() {
return TruststoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return TruststoreProviderFactory.class;
}
}

View file

@ -0,0 +1 @@
org.keycloak.connections.truststore.FileTruststoreProviderFactory

View file

@ -0,0 +1 @@
org.keycloak.connections.truststore.TruststoreSpi

View file

@ -131,7 +131,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId> <artifactId>keycloak-export-import-single-file</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId> <artifactId>keycloak-connections-http-client</artifactId>

View file

@ -45,9 +45,7 @@
}, },
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },
"connectionsJpa": { "connectionsJpa": {

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/> <module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.codehaus.jackson.jackson-core-asl"/> <module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/> <module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/> <module name="org.codehaus.jackson.jackson-xc"/>

View file

@ -16,6 +16,7 @@
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/> <module name="javax.api"/>
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-connections-truststore">
<resources>
<artifact name="${org.keycloak:keycloak-connections-truststore}"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
<module name="org.apache.httpcomponents"/>
</dependencies>
</module>

View file

@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -181,6 +181,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/> <maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/>
</module-def> </module-def>
<module-def name="org.keycloak.keycloak-connections-truststore">
<maven-resource group="org.keycloak" artifact="keycloak-connections-truststore"/>
</module-def>
<module-def name="org.keycloak.keycloak-model-jpa"> <module-def name="org.keycloak.keycloak-model-jpa">
<maven-resource group="org.keycloak" artifact="keycloak-model-jpa"/> <maven-resource group="org.keycloak" artifact="keycloak-model-jpa"/>
</module-def> </module-def>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/> <module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.codehaus.jackson.jackson-core-asl"/> <module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/> <module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/> <module name="org.codehaus.jackson.jackson-xc"/>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-common"/> <module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/> <module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/> <module name="javax.api"/>
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-connections-truststore">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
<module name="org.apache.httpcomponents"/>
</dependencies>
</module>

View file

@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -382,9 +382,7 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
By default the setting is like this: By default the setting is like this:
<programlisting><![CDATA[ <programlisting><![CDATA[
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },
]]></programlisting> ]]></programlisting>
Possible configuration options are: Possible configuration options are:
@ -421,15 +419,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>disable-trust-manager</term>
<listitem>
<para>
If true, HTTPS server certificates are not verified. If you set this to false, you must
configure a truststore.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term>disable-cookies</term> <term>disable-cookies</term>
<listitem> <listitem>
@ -439,44 +428,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>hostname-verification-policy</term>
<listitem>
<para>
<literal>WILDCARD</literal> by default. For HTTPS requests, this verifies the hostname
of the server's certificate. <literal>ANY</literal> means that the hostname is not verified.
<literal>WILDCARD</literal> Allows wildcards in subdomain names i.e. *.foo.com.
<literal>STRICT</literal> CN must match hostname exactly.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore</term>
<listitem>
<para>
The value is the file path to a Java keystore file. If
you prefix the path with <literal>classpath:</literal>, 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.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore-password</term>
<listitem>
<para>
Password for the truststore keystore.
This is
<emphasis>REQUIRED</emphasis>
if
<literal>truststore</literal>
is set.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term>client-keystore</term> <term>client-keystore</term>
<listitem> <listitem>
@ -516,13 +467,111 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</variablelist> </variablelist>
</para> </para>
</section> </section>
<section id="truststore">
<title>Securing Outgoing Server HTTP Requests</title>
<para>
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.
</para>
<para>
How certificates are validated is configured in the <literal>standalone/configuration/keycloak-server.json</literal>.
By default truststore provider is not configured, and any https connections fall back to standard java truststore
configuration as described in <ulink url="https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html">
Java's JSSE Reference Guide</ulink> - using <literal>javax.net.ssl.trustStore system property</literal>,
otherwise <literal>cacerts</literal> file that comes with java is used.
</para>
<para>
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.
</para>
<para>
You can add your truststore configuration by using the following template:
<programlisting><![CDATA[
"truststore": {
"file": {
"file": "path to your .jks file containing public certificates",
"password": "password",
"hostname-verification-policy": "WILDCARD",
"disabled": false
}
}
]]></programlisting>
</para>
<para>
Possible configuration options are:
<variablelist>
<varlistentry>
<term>file</term>
<listitem>
<para>
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
<emphasis>REQUIRED</emphasis>
if <literal>disabled</literal> is not true.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>password</term>
<listitem>
<para>
Password for the truststore.
This is
<emphasis>REQUIRED</emphasis>
if <literal>disabled</literal> is not true.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>hostname-verification-policy</term>
<listitem>
<para>
<literal>WILDCARD</literal> by default. For HTTPS requests, this verifies the hostname
of the server's certificate. <literal>ANY</literal> means that the hostname is not verified.
<literal>WILDCARD</literal> Allows wildcards in subdomain names i.e. *.foo.com.
<literal>STRICT</literal> CN must match hostname exactly.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>disabled</term>
<listitem>
<para>
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 <literal>file</literal>, and <literal>password</literal> for the truststore.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
You can use <emphasis>keytool</emphasis> to create a new truststore file and add trusted host certificates to it:
<programlisting>
$ keytool -import -alias HOSTDOMAIN -keystore truststore.jks -file host-certificate.cer
</programlisting>
</para>
</section>
<section id="ssl_modes"> <section id="ssl_modes">
<title>SSL/HTTPS Requirement/Modes</title> <title>SSL/HTTPS Requirement/Modes</title>
<warning> <warning>
<para> <para>
Keycloak is not set up by default to handle SSL/HTTPS in either the Keycloak is not set up by default to handle SSL/HTTPS. It is highly recommended that you either enable SSL
war distribution or appliance. It is highly recommended that you either enable SSL on the Keycloak server on the Keycloak server itself or on a reverse proxy in front of the Keycloak server.
itself or on a reverse proxy in front of the Keycloak server.
</para> </para>
</warning> </warning>
<para> <para>

View file

@ -467,6 +467,9 @@ public class LDAPOperationManager {
if (protocol != null) { if (protocol != null) {
env.put(Context.SECURITY_PROTOCOL, protocol); 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(); String bindDN = this.config.getBindDN();

View file

@ -640,6 +640,11 @@
<artifactId>keycloak-connections-mongo-update</artifactId> <artifactId>keycloak-connections-mongo-update</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId> <artifactId>keycloak-common</artifactId>

View file

@ -1,6 +1,9 @@
package org.keycloak.email; package org.keycloak.email;
import org.jboss.logging.Logger; 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.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -12,6 +15,8 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeMultipart;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -23,6 +28,12 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class); private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class);
private final KeycloakSession session;
public DefaultEmailSenderProvider(KeycloakSession session) {
this.session = session;
}
@Override @Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
try { try {
@ -52,6 +63,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
props.setProperty("mail.smtp.starttls.enable", "true"); props.setProperty("mail.smtp.starttls.enable", "true");
} }
if (ssl || starttls) {
setupTruststore(props);
}
props.setProperty("mail.smtp.timeout", "10000"); props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "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 @Override
public void close() { public void close() {
} }
} }

View file

@ -11,7 +11,7 @@ public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFac
@Override @Override
public EmailSenderProvider create(KeycloakSession session) { public EmailSenderProvider create(KeycloakSession session) {
return new DefaultEmailSenderProvider(); return new DefaultEmailSenderProvider(session);
} }
@Override @Override

View file

@ -72,9 +72,7 @@
}, },
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },
@ -99,5 +97,14 @@
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}", "databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}" "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}"
}
} }
} }

View file

@ -51,9 +51,7 @@
}, },
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },

View file

@ -43,6 +43,9 @@ public class LDAPEmbeddedServer {
private static final String DEFAULT_BIND_HOST = "localhost"; private static final String DEFAULT_BIND_HOST = "localhost";
private static final String DEFAULT_BIND_PORT = "10389"; private static final String DEFAULT_BIND_PORT = "10389";
private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif"; 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_INMEMORY = "mem";
public static final String DSF_FILE = "file"; public static final String DSF_FILE = "file";
@ -56,6 +59,9 @@ public class LDAPEmbeddedServer {
protected String ldifFile; protected String ldifFile;
protected String ldapSaslPrincipal; protected String ldapSaslPrincipal;
protected String directoryServiceFactory; protected String directoryServiceFactory;
protected boolean enableSSL = false;
protected String keystoreFile;
protected String certPassword;
protected DirectoryService directoryService; protected DirectoryService directoryService;
protected LdapServer ldapServer; protected LdapServer ldapServer;
@ -97,6 +103,9 @@ public class LDAPEmbeddedServer {
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE); this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE);
this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null); this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null);
this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF); 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) { protected String readProperty(String propertyName, String defaultValue) {
@ -194,6 +203,11 @@ public class LDAPEmbeddedServer {
// Read the transports // Read the transports
Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50);
if (enableSSL) {
ldap.setEnableSSL(true);
ldapServer.setKeystoreFile(keystoreFile);
ldapServer.setCertificatePassword(certPassword);
}
ldapServer.addTransports( ldap ); ldapServer.addTransports( ldap );
// Associate the DS to this LdapServer // Associate the DS to this LdapServer