diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java index 63cdab890c..e6d658800d 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java @@ -38,7 +38,7 @@ import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.keycloak.common.util.EnvUtil; import org.keycloak.common.util.KeystoreUtil; -import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.representations.adapters.config.AdapterHttpClientConfig; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -333,7 +333,7 @@ public class HttpClientBuilder { } } - public HttpClient build(AdapterConfig adapterConfig) { + public HttpClient build(AdapterHttpClientConfig adapterConfig) { disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing String truststorePath = adapterConfig.getTruststore(); @@ -379,13 +379,13 @@ public class HttpClientBuilder { /** * Configures a the proxy to use for auth-server requests if provided. *

- * If the given {@link AdapterConfig} contains the attribute {@code proxy-url} we use the + * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the * given URL as a proxy server, otherwise the proxy configuration is ignored. *

* * @param adapterConfig */ - private void configureProxyForAuthServerIfProvided(AdapterConfig adapterConfig) { + private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) { if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) { return; diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java new file mode 100644 index 0000000000..5c94fdb964 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.cloned; + +/** + * Configuration options relevant for configuring http client that can be used by adapter. + * + * NOTE: keep in sync with core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java until unified. + * + * @author hmlnarik + */ +public interface AdapterHttpClientConfig { + + /** + * Returns truststore filename. + */ + public String getTruststore(); + + /** + * Returns truststore password. + */ + public String getTruststorePassword(); + + /** + * Returns keystore with client keys. + */ + public String getClientKeystore(); + + /** + * Returns keystore password. + */ + public String getClientKeystorePassword(); + + /** + * Returns boolean flag whether any hostname verification is done on the server's + * certificate, {@code true} means that verification is not done. + * @return + */ + public boolean isAllowAnyHostname(); + + /** + * Returns boolean flag whether any trust management and hostname verification is done. + *

+ * NOTE Disabling trust manager is a security hole, so only set this option + * if you cannot or do not want to verify the identity of the + * host you are communicating with. + */ + public boolean isDisableTrustManager(); + + /** + * Returns size of connection pool. + */ + public int getConnectionPoolSize(); + + /** + * Returns URL of HTTP proxy. + */ + public String getProxyUrl(); + +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java new file mode 100644 index 0000000000..0fa330e5f6 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.cloned; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; + +import java.io.IOException; +import java.io.InputStream; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; +import org.keycloak.adapters.saml.descriptor.parsers.SamlDescriptorIDPKeysExtractor; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.saml.common.exceptions.ParsingException; + +/** + * @author Hynek Mlnařík + */ +public class HttpAdapterUtils { + + public static MultivaluedHashMap downloadKeysFromSamlDescriptor(HttpClient client, String descriptorUrl) throws HttpClientAdapterException { + try { + HttpGet httpRequest = new HttpGet(descriptorUrl); + HttpResponse response = client.execute(httpRequest); + int status = response.getStatusLine().getStatusCode(); + if (status != HttpStatus.SC_OK) { + EntityUtils.consumeQuietly(response.getEntity()); + throw new HttpClientAdapterException("Unexpected status = " + status); + } + + HttpEntity entity = response.getEntity(); + if (entity == null) { + throw new HttpClientAdapterException("There was no entity."); + } + + MultivaluedHashMap res; + try (InputStream is = entity.getContent()) { + res = extractKeysFromSamlDescriptor(is); + } + + EntityUtils.consumeQuietly(entity); + + return res; + } catch (IOException | ParsingException e) { + throw new HttpClientAdapterException("IO error", e); + } + } + + /** + * Parses SAML descriptor and extracts keys from it. + * @param xmlStream + * @return List of KeyInfo objects containing keys from the descriptor. + * @throws IOException + */ + public static MultivaluedHashMap extractKeysFromSamlDescriptor(InputStream xmlStream) throws ParsingException { + Object res = new SamlDescriptorIDPKeysExtractor().parse(xmlStream); + return (MultivaluedHashMap) res; + } + +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java new file mode 100644 index 0000000000..e0371adc05 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.cloned; + +/** + * @author Marek Posolda + */ +public class HttpClientAdapterException extends Exception { + + public HttpClientAdapterException(String message) { + super(message); + } + + public HttpClientAdapterException(String message, Throwable t) { + super(message, t); + } +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java new file mode 100644 index 0000000000..7e26c01488 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java @@ -0,0 +1,396 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.cloned; + +import org.apache.http.HttpHost; +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.SingleClientConnManager; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.keycloak.common.util.EnvUtil; +import org.keycloak.common.util.KeystoreUtil; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.URI; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Abstraction for creating HttpClients. Allows SSL configuration. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class HttpClientBuilder { + public static 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 + } + + + /** + * @author Bill Burke + * @version $Revision: 1 $ + */ + private static class PassthroughTrustManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + protected KeyStore truststore; + protected KeyStore clientKeyStore; + protected String clientPrivateKeyPassword; + protected boolean disableTrustManager; + protected boolean disableCookieCache = true; + protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD; + protected SSLContext sslContext; + protected int connectionPoolSize = 100; + protected int maxPooledPerRoute = 0; + protected long connectionTTL = -1; + protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS; + protected HostnameVerifier verifier = null; + protected long socketTimeout = -1; + protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS; + protected long establishConnectionTimeout = -1; + protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS; + protected HttpHost proxyHost; + + + /** + * Socket inactivity timeout + * + * @param timeout + * @param unit + * @return + */ + public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit) { + this.socketTimeout = timeout; + this.socketTimeoutUnits = unit; + return this; + } + + /** + * When trying to make an initial socket connection, what is the timeout? + * + * @param timeout + * @param unit + * @return + */ + public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) { + this.establishConnectionTimeout = timeout; + this.establishConnectionTimeoutUnits = unit; + return this; + } + + public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) { + this.connectionTTL = ttl; + this.connectionTTLUnit = unit; + return this; + } + + public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) { + this.maxPooledPerRoute = maxPooledPerRoute; + return this; + } + + public HttpClientBuilder connectionPoolSize(int connectionPoolSize) { + this.connectionPoolSize = connectionPoolSize; + return this; + } + + /** + * Disable trust management and hostname verification. NOTE this is a security + * hole, so only set this option if you cannot or do not want to verify the identity of the + * host you are communicating with. + */ + public HttpClientBuilder disableTrustManager() { + this.disableTrustManager = true; + return this; + } + + public HttpClientBuilder disableCookieCache() { + this.disableCookieCache = true; + return this; + } + + /** + * SSL policy used to verify hostnames + * + * @param policy + * @return + */ + public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) { + this.policy = policy; + return this; + } + + + public HttpClientBuilder sslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public HttpClientBuilder trustStore(KeyStore truststore) { + this.truststore = truststore; + return this; + } + + public HttpClientBuilder keyStore(KeyStore keyStore, String password) { + this.clientKeyStore = keyStore; + this.clientPrivateKeyPassword = password; + return this; + } + + public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) { + this.clientKeyStore = keyStore; + this.clientPrivateKeyPassword = new String(password); + return this; + } + + + static class VerifierWrapper implements X509HostnameVerifier { + protected HostnameVerifier verifier; + + VerifierWrapper(HostnameVerifier verifier) { + this.verifier = verifier; + } + + @Override + public void verify(String host, SSLSocket ssl) throws IOException { + if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure"); + } + + @Override + public void verify(String host, X509Certificate cert) throws SSLException { + throw new SSLException("This verification path not implemented"); + } + + @Override + public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException { + throw new SSLException("This verification path not implemented"); + } + + @Override + public boolean verify(String s, SSLSession sslSession) { + return verifier.verify(s, sslSession); + } + } + + public HttpClient build() { + X509HostnameVerifier verifier = null; + if (this.verifier != null) verifier = new VerifierWrapper(this.verifier); + else { + switch (policy) { + case ANY: + verifier = new AllowAllHostnameVerifier(); + break; + case WILDCARD: + verifier = new BrowserCompatHostnameVerifier(); + break; + case STRICT: + verifier = new StrictHostnameVerifier(); + break; + } + } + try { + SSLSocketFactory sslsf = null; + SSLContext theContext = sslContext; + if (disableTrustManager) { + theContext = SSLContext.getInstance("SSL"); + theContext.init(null, new TrustManager[]{new PassthroughTrustManager()}, + new SecureRandom()); + verifier = new AllowAllHostnameVerifier(); + sslsf = new SniSSLSocketFactory(theContext, verifier); + } else if (theContext != null) { + sslsf = new SniSSLSocketFactory(theContext, verifier); + } else if (clientKeyStore != null || truststore != null) { + sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier); + } else { + final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS); + tlsContext.init(null, null, null); + sslsf = new SniSSLSocketFactory(tlsContext, verifier); + } + SchemeRegistry registry = new SchemeRegistry(); + registry.register( + new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); + Scheme httpsScheme = new Scheme("https", 443, sslsf); + registry.register(httpsScheme); + ClientConnectionManager cm = null; + if (connectionPoolSize > 0) { + ThreadSafeClientConnManager tcm = new ThreadSafeClientConnManager(registry, connectionTTL, connectionTTLUnit); + tcm.setMaxTotal(connectionPoolSize); + if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize; + tcm.setDefaultMaxPerRoute(maxPooledPerRoute); + cm = tcm; + + } else { + cm = new SingleClientConnManager(registry); + } + BasicHttpParams params = new BasicHttpParams(); + + if (proxyHost != null) { + params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost); + } + + if (socketTimeout > -1) { + HttpConnectionParams.setSoTimeout(params, (int) socketTimeoutUnits.toMillis(socketTimeout)); + + } + if (establishConnectionTimeout > -1) { + HttpConnectionParams.setConnectionTimeout(params, (int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout)); + } + DefaultHttpClient client = new DefaultHttpClient(cm, params); + + if (disableCookieCache) { + client.setCookieStore(new CookieStore() { + @Override + public void addCookie(Cookie cookie) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public List getCookies() { + return Collections.emptyList(); + } + + @Override + public boolean clearExpired(Date date) { + return false; //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void clear() { + //To change body of implemented methods use File | Settings | File Templates. + } + }); + + } + return client; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public HttpClient build(AdapterHttpClientConfig adapterConfig) { + disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing + + String truststorePath = adapterConfig.getTruststore(); + if (truststorePath != null) { + truststorePath = EnvUtil.replace(truststorePath); + String truststorePassword = adapterConfig.getTruststorePassword(); + try { + this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load truststore", e); + } + } + String clientKeystore = adapterConfig.getClientKeystore(); + if (clientKeystore != null) { + clientKeystore = EnvUtil.replace(clientKeystore); + String clientKeystorePassword = adapterConfig.getClientKeystorePassword(); + try { + KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); + keyStore(clientCertKeystore, clientKeystorePassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load keystore", e); + } + } + int size = 10; + if (adapterConfig.getConnectionPoolSize() > 0) + size = adapterConfig.getConnectionPoolSize(); + HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD; + if (adapterConfig.isAllowAnyHostname()) + policy = HttpClientBuilder.HostnameVerificationPolicy.ANY; + connectionPoolSize(size); + hostnameVerification(policy); + if (adapterConfig.isDisableTrustManager()) { + disableTrustManager(); + } else { + trustStore(truststore); + } + + configureProxyForAuthServerIfProvided(adapterConfig); + + return build(); + } + + /** + * Configures a the proxy to use for auth-server requests if provided. + *

+ * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the + * given URL as a proxy server, otherwise the proxy configuration is ignored. + *

+ * + * @param adapterConfig + */ + private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) { + + if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) { + return; + } + + URI uri = URI.create(adapterConfig.getProxyUrl()); + this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()); + } +} \ No newline at end of file diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java new file mode 100644 index 0000000000..8c064b0ded --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.cloned; + +import org.apache.http.HttpHost; +import org.apache.http.conn.scheme.HostNameResolver; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.protocol.HttpContext; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.AccessController; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * SSLSocketFactory that uses Server Name Indication (SNI) TLS extension. + * + *

+ * Originally copied from keycloak-adapter-core project. + * + * @author Marko Strukelj + * @author Hynek Mlnařík + */ +public class SniSSLSocketFactory extends SSLSocketFactory { + + private static final Logger LOG = Logger.getLogger(SniSSLSocketFactory.class.getName()); + + public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, HostNameResolver nameResolver) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(algorithm, keystore, keyPassword, truststore, random, nameResolver); + } + + public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(algorithm, keystore, keyPassword, truststore, random, trustStrategy, hostnameVerifier); + } + + public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(algorithm, keystore, keyPassword, truststore, random, hostnameVerifier); + } + + public SniSSLSocketFactory(KeyStore keystore, String keystorePassword, KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(keystore, keystorePassword, truststore); + } + + public SniSSLSocketFactory(KeyStore keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(keystore, keystorePassword); + } + + public SniSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + } + + public SniSSLSocketFactory(TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(trustStrategy, hostnameVerifier); + } + + public SniSSLSocketFactory(TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(trustStrategy); + } + + public SniSSLSocketFactory(SSLContext sslContext) { + super(sslContext); + } + + public SniSSLSocketFactory(SSLContext sslContext, HostNameResolver nameResolver) { + super(sslContext, nameResolver); + } + + public SniSSLSocketFactory(SSLContext sslContext, X509HostnameVerifier hostnameVerifier) { + super(sslContext, hostnameVerifier); + } + + public SniSSLSocketFactory(SSLContext sslContext, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) { + super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) { + super(socketfactory, hostnameVerifier); + } + + public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) { + super(socketfactory, supportedProtocols, supportedCipherSuites, hostnameVerifier); + } + + @Override + public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException { + return super.connectSocket(connectTimeout, applySNI(socket, host.getHostName()), host, remoteAddress, localAddress, context); + } + + @Override + public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException { + return super.createLayeredSocket(applySNI(socket, target), target, port, context); + } + + private Socket applySNI(final Socket socket, String hostname) { + if (socket instanceof SSLSocket) { + try { + Method setHostMethod = AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Method run() throws NoSuchMethodException { + return socket.getClass().getMethod("setHost", String.class); + } + }); + + setHostMethod.invoke(socket, hostname); + LOG.log(Level.FINEST, "Applied SNI to socket for host {0}", hostname); + } catch (PrivilegedActionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e); + } + } + return socket; + } +} diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java new file mode 100644 index 0000000000..2c03ef884d --- /dev/null +++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java @@ -0,0 +1,82 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.keycloak.adapters.cloned; + +import java.io.InputStream; +import java.security.cert.X509Certificate; +import java.util.List; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyName; +import javax.xml.crypto.dsig.keyinfo.X509Data; +import static org.hamcrest.CoreMatchers.*; +import org.junit.Test; +import static org.junit.Assert.*; +import org.keycloak.adapters.saml.config.parsers.ConfigXmlConstants; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.dom.saml.v2.metadata.KeyTypes; +import org.keycloak.saml.common.exceptions.ParsingException; + +/** + * + * @author hmlnarik + */ +public class HttpAdapterUtilsTest { + + private T getContent(List objects, Class clazz) { + for (Object o : objects) { + if (clazz.isInstance(o)) { + return (T) o; + } + } + return null; + } + + @Test + public void testExtractKeysFromSamlDescriptor() throws ParsingException { + InputStream xmlStream = HttpAdapterUtilsTest.class.getResourceAsStream("saml-descriptor-valid.xml"); + MultivaluedHashMap res = HttpAdapterUtils.extractKeysFromSamlDescriptor(xmlStream); + + assertThat(res, notNullValue()); + assertThat(res.keySet(), hasItems(KeyTypes.SIGNING.value())); + assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR), notNullValue()); + assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR).size(), equalTo(2)); + + KeyInfo ki; + KeyName keyName; + X509Data x509data; + X509Certificate x509certificate; + + ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(0); + assertThat(ki.getContent().size(), equalTo(2)); + assertThat((List) ki.getContent(), hasItem(instanceOf(X509Data.class))); + assertThat((List) ki.getContent(), hasItem(instanceOf(KeyName.class))); + + keyName = getContent(ki.getContent(), KeyName.class); + assertThat(keyName.getName(), equalTo("rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI")); + + x509data = getContent(ki.getContent(), X509Data.class); + assertThat(x509data, notNullValue()); + x509certificate = getContent(x509data.getContent(), X509Certificate.class); + assertThat(x509certificate, notNullValue()); + assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA")); + + ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(1); + assertThat(ki.getContent().size(), equalTo(2)); + assertThat((List) ki.getContent(), hasItem(instanceOf(X509Data.class))); + assertThat((List) ki.getContent(), hasItem(instanceOf(KeyName.class))); + + keyName = getContent(ki.getContent(), KeyName.class); + assertThat(keyName.getName(), equalTo("BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4")); + + x509data = getContent(ki.getContent(), X509Data.class); + assertThat(x509data, notNullValue()); + x509certificate = getContent(x509data.getContent(), X509Certificate.class); + assertThat(x509certificate, notNullValue()); + assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA")); + + } + +} diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml new file mode 100644 index 0000000000..8dd3e41ff7 --- /dev/null +++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml @@ -0,0 +1,62 @@ + + + + + + + + rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI + + + MIICmzCCAYMCBgFX/9ccIDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMjUwWhcNMjYxMDI2MDgxNDMwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPjDrM890OoFWLIU5xNT+v8B8EkpOGY1y/9Yi/yQd95uG/p5LaywiPsw+lPy4tSn1pH/2SxNDST2zynKPDd1lYDev43m0sC2FfD2H73q3udQRqSOxW1e8FrTrGDIHxb82UNrCPlu+fH+xYSkigrkOvLvPigTwSIcu8vgs0lk9FqJ81ty3Wj2e9lS7JJGAJ3pC7rp39VLdJSKbfyj/v2RYBeG5Pscncl8cjUOHUq5u19hThjkU2jOBzgIK2JS0bNmzSfH1eBTZMoCQBI1UJ1IbA8tqjQwpOXc+JkPBRU8T/JUQoQlSR6DTcPFvDgH2oGZYFHFfUontZqtz8jrIt2pxBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK5VgQp1x1FKgabFI6W/iGuy9ZCRoAixOOEGGkDps6dOEFgTQKTy5D/FZts9KuNxhhiD+NvS0d5BKYa5ITPLVPnGucgYkZhz+/+GhxmbjeQr0eJPaY7ZgLfH3tPA6tfdIkA0iE1En1sKEwt6R6DZjh9jtP9laoUoddTvYaFLJpZ2u1Ik94q6ZqX0fS/RKchaBHjhg6MtqCcHt07CBKHh8XNmKPXVSJC/p0MjyXv+qLaNNqyaAvAw6P6DX1hNjzrdkuaaHGXhu6kkezZUVlDWAm9cd1ppqalSK6ggy7yMW1NWTd/NYOPsFU2TS8DDPzRo14s1Qvw4v+TY6yT0NURJPQA= + + + + + + + BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4 + + + MIICmzCCAYMCBgFX/9eK7TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMzE4WhcNMjYxMDI2MDgxNDU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCDLT+40/BWzWPSVmpaSaZRs5lBMQ9VP9TCoXkby4PHqxIWRecTPM8fcNkPNPE/tiR2tUIpMXPDzgXNFA/EMoB3V1OEVXPecjKtiZczdR6pi75CBx7PJ2fSXg6xpjhZmHu0k7x591GZdP8Iiu2E6b9QA2p5VXgNgfuP07XzgabnSvIrLG60Imus3u6C2qA/QEuY7EYQWrFooriYLW6B8s3xU8R1a92SLMT8JsfMWXi+1CzAhIbVvdwUwkhVDDhAU6pUek88QQgxodd3FAMksoijCGFN1yrCkovlFhKb3j9AC6Icd9eeJuwYddN/nMeMGEDOeCcAGBACiaUisjUvZDw1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHAHbBI0CRfdw5ZHxHAjgSQvSj41c/4cfwln4Q7X3I5lMBbW3tcgond6Ku9eU46FzG5VpgXIgvEf4u0O9jUnxLlO50+t2SHwQ1RwHdBWQngVSZCRzscq3KrSzx1hx88qLyqcPrr3QtR92fYipDjENxttT/qJtDMrXlwLZEITlHDoneX319USYB9C4zlrCIsQ5XxQTTyCx886Pz15DSVSRxVp61HGk6ROsX/DG5/xwInlzgMZ0r3JWnAjtAaXqUrcwH9FXxco+xkiqKW79bGhWGQI9sXXvQSSNAaENMIUhxtd9uOi1l5e0EkKHE2fHlYyfdUDnFJWwSMXd/NM+hVI4Lw= + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + + + + \ No newline at end of file diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index c4818b45c7..0ba327d733 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -39,7 +39,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "policy-enforcer" }) -public class AdapterConfig extends BaseAdapterConfig { +public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig { @JsonProperty("allow-any-hostname") protected boolean allowAnyHostname; @@ -82,6 +82,7 @@ public class AdapterConfig extends BaseAdapterConfig { @JsonProperty("proxy-url") protected String proxyUrl; + @Override public boolean isAllowAnyHostname() { return allowAnyHostname; } @@ -90,6 +91,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.allowAnyHostname = allowAnyHostname; } + @Override public boolean isDisableTrustManager() { return disableTrustManager; } @@ -98,6 +100,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.disableTrustManager = disableTrustManager; } + @Override public String getTruststore() { return truststore; } @@ -106,6 +109,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.truststore = truststore; } + @Override public String getTruststorePassword() { return truststorePassword; } @@ -114,6 +118,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.truststorePassword = truststorePassword; } + @Override public String getClientKeystore() { return clientKeystore; } @@ -122,6 +127,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.clientKeystore = clientKeystore; } + @Override public String getClientKeystorePassword() { return clientKeystorePassword; } @@ -138,6 +144,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.clientKeyPassword = clientKeyPassword; } + @Override public int getConnectionPoolSize() { return connectionPoolSize; } @@ -202,6 +209,7 @@ public class AdapterConfig extends BaseAdapterConfig { this.policyEnforcerConfig = policyEnforcerConfig; } + @Override public String getProxyUrl() { return proxyUrl; } diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java new file mode 100644 index 0000000000..fa4c87e5c6 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.adapters.config; + +/** + * Configuration options relevant for configuring http client that can be used by adapter. + * + * NOTE: keep in sync with adapters/saml/core/src/main/java/org/keycloak/adapters/AdapterHttpClientConfig.java until unified. + * + * @author hmlnarik + */ +public interface AdapterHttpClientConfig { + + /** + * Returns truststore filename. + */ + public String getTruststore(); + + /** + * Returns truststore password. + */ + public String getTruststorePassword(); + + /** + * Returns keystore with client keys. + */ + public String getClientKeystore(); + + /** + * Returns keystore password. + */ + public String getClientKeystorePassword(); + + /** + * Returns boolean flag whether any hostname verification is done on the server's + * certificate, {@code true} means that verification is not done. + * @return + */ + public boolean isAllowAnyHostname(); + + /** + * Returns boolean flag whether any trust management and hostname verification is done. + *

+ * NOTE Disabling trust manager is a security hole, so only set this option + * if you cannot or do not want to verify the identity of the + * host you are communicating with. + */ + public boolean isDisableTrustManager(); + + /** + * Returns size of connection pool. + */ + public int getConnectionPoolSize(); + + /** + * Returns URL of HTTP proxy. + */ + public String getProxyUrl(); + +}