Merge pull request #2000 from mstruk/truststore
KEYCLOAK-1717 Truststore SPI and file provider
This commit is contained in:
commit
a6c852603e
41 changed files with 766 additions and 106 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
35
connections/truststore/pom.xml
Executable 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>
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.connections.truststore.FileTruststoreProviderFactory
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.connections.truststore.TruststoreSpi
|
5
dependencies/server-min/pom.xml
vendored
5
dependencies/server-min/pom.xml
vendored
|
@ -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>
|
||||||
|
|
|
@ -45,9 +45,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"connectionsHttpClient": {
|
"connectionsHttpClient": {
|
||||||
"default": {
|
"default": {}
|
||||||
"disable-trust-manager": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"connectionsJpa": {
|
"connectionsJpa": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
|
@ -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"/>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -51,9 +51,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"connectionsHttpClient": {
|
"connectionsHttpClient": {
|
||||||
"default": {
|
"default": {}
|
||||||
"disable-trust-manager": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue