diff --git a/connections/http-client/pom.xml b/connections/http-client/pom.xml new file mode 100755 index 0000000000..f2015695da --- /dev/null +++ b/connections/http-client/pom.xml @@ -0,0 +1,36 @@ + + + + keycloak-parent + org.keycloak + 1.2.0.RC1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-connections-http-client + Keycloak Connections Apache HttpClient + + + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-model-api + + + org.apache.httpcomponents + httpclient + provided + + + org.jboss.logging + jboss-logging + provided + + + diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java new file mode 100755 index 0000000000..703660fce8 --- /dev/null +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java @@ -0,0 +1,139 @@ +package org.keycloak.connections.httpclient; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.EntityBuilder; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.CloseableHttpClient; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.util.EnvUtil; +import org.keycloak.util.KeystoreUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.concurrent.TimeUnit; + +/** + * @author Stian Thorgersen + */ +public class DefaultHttpClientFactory implements HttpClientFactory { + + private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class); + + private volatile CloseableHttpClient httpClient; + + @Override + public HttpClientProvider create(KeycloakSession session) { + return new HttpClientProvider() { + @Override + public HttpClient getHttpClient() { + return httpClient; + } + + @Override + public void close() { + + } + + @Override + public int postText(String uri, String text) throws IOException { + HttpPost request = new HttpPost(uri); + request.setEntity(EntityBuilder.create().setText(text).setContentType(ContentType.TEXT_PLAIN).build()); + HttpResponse response = httpClient.execute(request); + try { + return response.getStatusLine().getStatusCode(); + } finally { + HttpEntity entity = response.getEntity(); + if (entity != null) { + InputStream is = entity.getContent(); + if (is != null) is.close(); + } + + } + } + + @Override + public InputStream get(String uri) throws IOException { + HttpGet request = new HttpGet(uri); + HttpResponse response = httpClient.execute(request); + HttpEntity entity = response.getEntity(); + if (entity == null) return null; + return entity.getContent(); + + } + }; + } + + @Override + public void close() { + try { + httpClient.close(); + } catch (IOException e) { + + } + } + + @Override + public String getId() { + return "default"; + } + + @Override + public void init(Config.Scope config) { + long socketTimeout = config.getLong("socket-timeout-millis", -1L); + long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L); + int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0); + int connectionPoolSize = config.getInt("connection-pool-size", 200); + boolean disableTrustManager = config.getBoolean("disable-trust-manager", false); + boolean disableCookies = config.getBoolean("disable-cookies", true); + String hostnameVerificationPolicy = config.get("hostname-verification-policy", "WILDCARD"); + HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = HttpClientBuilder.HostnameVerificationPolicy.valueOf(hostnameVerificationPolicy); + String truststore = config.get("truststore"); + String truststorePassword = config.get("truststore-password"); + String clientKeystore = config.get("client-keystore"); + String clientKeystorePassword = config.get("client-keystore-password"); + String clientPrivateKeyPassword = config.get("client-key-password"); + + HttpClientBuilder builder = new HttpClientBuilder(); + builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) + .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) + .maxPooledPerRoute(maxPooledPerRoute) + .connectionPoolSize(connectionPoolSize) + .hostnameVerification(hostnamePolicy) + .disableCookies(disableCookies); + if (disableTrustManager) builder.disableTrustManager(); + if (truststore != null) { + truststore = EnvUtil.replace(truststore); + try { + builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword)); + } catch (Exception e) { + throw new RuntimeException("Failed to load truststore", e); + } + } + if (clientKeystore != null) { + clientKeystore = EnvUtil.replace(clientKeystore); + try { + KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword); + builder.keyStore(clientCertKeystore, clientPrivateKeyPassword); + } catch (Exception e) { + throw new RuntimeException("Failed to load keystore", e); + } + } + httpClient = builder.build(); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + + +} diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java new file mode 100755 index 0000000000..7459f4d846 --- /dev/null +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java @@ -0,0 +1,281 @@ +package org.keycloak.connections.httpclient; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContexts; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +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.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +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 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 boolean disableCookies = false; + + + /** + * 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; + } + + /** + * Disable cookie management. + */ + public HttpClientBuilder disableCookies(boolean disable) { + this.disableTrustManager = disable; + 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 CloseableHttpClient 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 { + SSLConnectionSocketFactory sslsf = null; + SSLContext theContext = sslContext; + if (disableTrustManager) { + theContext = SSLContext.getInstance("TLS"); + theContext.init(null, new TrustManager[]{new PassthroughTrustManager()}, + new SecureRandom()); + verifier = new AllowAllHostnameVerifier(); + sslsf = new SSLConnectionSocketFactory(theContext, verifier); + } else if (theContext != null) { + sslsf = new SSLConnectionSocketFactory(theContext, verifier); + } else if (clientKeyStore != null || truststore != null) { + theContext = createSslContext("TLS", clientKeyStore, clientPrivateKeyPassword, truststore, null); + sslsf = new SSLConnectionSocketFactory(theContext, verifier); + } else { + final SSLContext tlsContext = SSLContext.getInstance("TLS"); + tlsContext.init(null, null, null); + sslsf = new SSLConnectionSocketFactory(tlsContext, verifier); + } + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout((int) establishConnectionTimeout) + .setSocketTimeout((int) socketTimeout).build(); + + org.apache.http.impl.client.HttpClientBuilder builder = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setSSLSocketFactory(sslsf) + .setMaxConnTotal(connectionPoolSize) + .setMaxConnPerRoute(maxPooledPerRoute); + if (disableCookies) builder.disableCookieManagement(); + return builder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private SSLContext createSslContext( + final String algorithm, + final KeyStore keystore, + final String keyPassword, + final KeyStore truststore, + final SecureRandom random) + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + return SSLContexts.custom() + .useProtocol(algorithm) + .setSecureRandom(random) + .loadKeyMaterial(keystore, keyPassword != null ? keyPassword.toCharArray() : null) + .loadTrustMaterial(truststore) + .build(); + } + +} \ No newline at end of file diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientFactory.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientFactory.java new file mode 100755 index 0000000000..2172b4e9cf --- /dev/null +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientFactory.java @@ -0,0 +1,11 @@ +package org.keycloak.connections.httpclient; + +import org.apache.http.client.HttpClient; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface HttpClientFactory extends ProviderFactory { +} diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java new file mode 100755 index 0000000000..d897dd2f04 --- /dev/null +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientProvider.java @@ -0,0 +1,34 @@ +package org.keycloak.connections.httpclient; + +import org.apache.http.client.HttpClient; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Stian Thorgersen + */ +public interface HttpClientProvider extends Provider { + HttpClient getHttpClient(); + + /** + * Helper method + * + * @param uri + * @param text + * @return http response status + * @throws IOException + */ + public int postText(String uri, String text) throws IOException; + + /** + * Helper method + * + * @param uri + * @return response stream, you must close this stream or leaks will happen + * @throws IOException + */ + public InputStream get(String uri) throws IOException; +} diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientSpi.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientSpi.java new file mode 100755 index 0000000000..d9f42287c2 --- /dev/null +++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.connections.httpclient; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class HttpClientSpi implements Spi { + + @Override + public String getName() { + return "connectionsHttpClient"; + } + + @Override + public Class getProviderClass() { + return HttpClientProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return HttpClientFactory.class; + } + +} diff --git a/connections/http-client/src/main/resources/META-INF/services/org.keycloak.connections.httpclient.HttpClientFactory b/connections/http-client/src/main/resources/META-INF/services/org.keycloak.connections.httpclient.HttpClientFactory new file mode 100755 index 0000000000..48a58709c7 --- /dev/null +++ b/connections/http-client/src/main/resources/META-INF/services/org.keycloak.connections.httpclient.HttpClientFactory @@ -0,0 +1 @@ +org.keycloak.connections.httpclient.DefaultHttpClientFactory \ No newline at end of file diff --git a/connections/http-client/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/connections/http-client/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100755 index 0000000000..d91ed393d1 --- /dev/null +++ b/connections/http-client/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.connections.httpclient.HttpClientSpi diff --git a/connections/pom.xml b/connections/pom.xml index 4e102106b5..b701354720 100755 --- a/connections/pom.xml +++ b/connections/pom.xml @@ -19,6 +19,7 @@ mongo file mongo-update + http-client diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/FindFile.java b/core/src/main/java/org/keycloak/util/FindFile.java similarity index 95% rename from integration/adapter-core/src/main/java/org/keycloak/adapters/FindFile.java rename to core/src/main/java/org/keycloak/util/FindFile.java index 19d8f83df6..07edebcc06 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/FindFile.java +++ b/core/src/main/java/org/keycloak/util/FindFile.java @@ -1,4 +1,4 @@ -package org.keycloak.adapters; +package org.keycloak.util; import org.keycloak.constants.GenericConstants; diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml index aa886a629a..0165e72390 100755 --- a/dependencies/server-min/pom.xml +++ b/dependencies/server-min/pom.xml @@ -135,6 +135,11 @@ org.keycloak keycloak-export-import-single-file + + + org.keycloak + keycloak-connections-http-client + diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 84c3ead330..0fdcfa481c 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -169,6 +169,10 @@ + + + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml new file mode 100755 index 0000000000..1d35246514 --- /dev/null +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-saml-protocol/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-saml-protocol/main/module.xml index b49da9aa59..655a8c0c91 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-saml-protocol/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-saml-protocol/main/module.xml @@ -21,6 +21,7 @@ + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index aa751122f2..55a5518dad 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -14,6 +14,7 @@ + diff --git a/distribution/subsystem-war/src/main/resources/META-INF/keycloak-server.json b/distribution/subsystem-war/src/main/resources/META-INF/keycloak-server.json index 9f0d03ea5d..3e1896c90b 100755 --- a/distribution/subsystem-war/src/main/resources/META-INF/keycloak-server.json +++ b/distribution/subsystem-war/src/main/resources/META-INF/keycloak-server.json @@ -63,6 +63,12 @@ "interval": 900 }, + "connectionsHttpClient": { + "default": { + "disable-trust-manager": true + } + }, + "connectionsJpa": { "default": { "dataSource": "java:jboss/datasources/KeycloakDS", diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml index 5b4b043fe9..0c419b7101 100755 --- a/docbook/reference/en/en-US/modules/server-installation.xml +++ b/docbook/reference/en/en-US/modules/server-installation.xml @@ -395,6 +395,150 @@ All configuration options are optional. Default value for directory is +
+ Outgoing Server HTTP Requests + + Keycloak server needs to invoke on remote HTTP endpoints to do things like backchannel logouts and other + management functions. Keycloak maintains a HTTP client connection pool which has various configuration + settings you can specify before boot time. This is configured in the + standalone/configuration/keycloak-server.json. + By default the setting is like this: + + Possible configuration options are: + + + establish-connection-timeout-millis + + + Timeout for establishing a socket connection. + + + + + socket-timeout-millis + + + If an outgoing request does not receive data for this amount of time, timeout the connection. + + + + + connection-pool-size + + + How many connections can be in the pool. + + + + + max-pooled-per-route + + + How many connections can be pooled per host. + + + + + disable-trust-manager + + + If true, HTTPS server certificates are not verified. If you set this to false, you must + configure a truststore. + + + + + disable-cookies + + + true by default. When set to true, this will disable any cookie + caching. + + + + + hostname-verification-policy + + + WILDCARD by default. For HTTPS requests, this verifies the hostname + of the server's certificate. ANY means that the hostname is not verified. + WILDCARD Allows wildcards in subdomain names i.e. *.foo.com. + STRICT CN must match hostname exactly. + + + + + truststore + + + The value is the file path to a Java keystore file. If + you prefix the path with classpath:, then the truststore will be obtained + from the deployment's classpath instead. + HTTPS + requests need a way to verify the host of the server they are talking to. This is + what the trustore does. The keystore contains one or more trusted + host certificates or certificate authorities. + + + + + truststore-password + + + Password for the truststore keystore. + This is + REQUIRED + if + truststore + is set. + + + + + client-keystore + + + This is the file path to a Java keystore file. + This keystore contains client certificate for two-way SSL. + + + + + client-keystore-password + + + Password for the client keystore. + This is + REQUIRED + if + client-keystore + is set. + + + + + client-key-password + + + Not supported yet, but we will support in future versions. + Password for the client's key. + This is + REQUIRED + if + client-keystore + is set. + + + + + +
SSL/HTTPS Requirement/Modes diff --git a/examples/broker/facebook-authentication/pom.xml b/examples/broker/facebook-authentication/pom.xml index f6884d5468..c089835d83 100755 --- a/examples/broker/facebook-authentication/pom.xml +++ b/examples/broker/facebook-authentication/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - keycloak-examples-broker-parent + keycloak-examples-parent org.keycloak 1.2.0.RC1-SNAPSHOT diff --git a/examples/broker/google-authentication/pom.xml b/examples/broker/google-authentication/pom.xml index 6060f3de24..1ca7ced2a2 100755 --- a/examples/broker/google-authentication/pom.xml +++ b/examples/broker/google-authentication/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - keycloak-examples-broker-parent + keycloak-examples-parent org.keycloak 1.2.0.RC1-SNAPSHOT diff --git a/examples/broker/saml-broker-authentication/pom.xml b/examples/broker/saml-broker-authentication/pom.xml index e3945f573d..7318e93811 100755 --- a/examples/broker/saml-broker-authentication/pom.xml +++ b/examples/broker/saml-broker-authentication/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - keycloak-examples-broker-parent + keycloak-examples-parent org.keycloak 1.2.0.RC1-SNAPSHOT diff --git a/examples/broker/twitter-authentication/pom.xml b/examples/broker/twitter-authentication/pom.xml index 3e730b7cc0..1e4d425ad8 100755 --- a/examples/broker/twitter-authentication/pom.xml +++ b/examples/broker/twitter-authentication/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - keycloak-examples-broker-parent + keycloak-examples-parent org.keycloak 1.2.0.RC1-SNAPSHOT diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties old mode 100644 new mode 100755 index 5999681766..d99be80159 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties @@ -138,6 +138,7 @@ failedToProcessResponseMessage=Konnte Response nicht verarbeiten. httpsRequiredMessage=HTTPS erforderlich. realmNotEnabledMessage=Realm nicht aktiviert. invalidRequestMessage=Ung\u00FCltiger Request. +failedLogout=Logout failed unknownLoginRequesterMessage=Ung\u00FCltiger login requester loginRequesterNotEnabledMessage=Login requester nicht aktiviert. bearerOnlyMessage=Bearer-only Applikationen k\u00F6nne sich nicht via Browser anmelden. diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties index 98985ca29c..9ee2d07643 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -140,6 +140,7 @@ failedToProcessResponseMessage=Failed to process response httpsRequiredMessage=HTTPS required realmNotEnabledMessage=Realm not enabled invalidRequestMessage=Invalid Request +failedLogout=Logout failed unknownLoginRequesterMessage=Unknown login requester loginRequesterNotEnabledMessage=Login requester not enabled bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties index b019d6cdd0..8f7be077ed 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties @@ -135,6 +135,7 @@ failedToProcessResponseMessage=Fallimento nell''elaborazione della risposta httpsRequiredMessage=HTTPS richiesto realmNotEnabledMessage=Realm non abilitato invalidRequestMessage=Richiesta non valida +failedLogout=Logout failed unknownLoginRequesterMessage=Richiedente di Login non riconosciuto loginRequesterNotEnabledMessage=Richiedente di Login non abilitato bearerOnlyMessage=Alle applicazioni di tipo Bearer-only non e'' consentito di effettuare il login tramite browser diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties old mode 100644 new mode 100755 index 8d50d3e297..45b4489c02 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -135,6 +135,7 @@ failedToProcessResponseMessage=Falha ao processar a resposta httpsRequiredMessage=HTTPS requerido realmNotEnabledMessage=Realm desativado invalidRequestMessage=Pedido inv\u00E1lido +failedLogout=Logout failed unknownLoginRequesterMessage=Solicitante de login desconhecido loginRequesterNotEnabledMessage=Solicitante de login desativado bearerOnlyMessage=Aplica\u00E7\u00F5es somente ao portador n\u00E3o tem permiss\u00E3o para iniciar o login pelo navegador diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java old mode 100644 new mode 100755 index 5b6c55d5e8..152a9d8117 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java @@ -24,7 +24,7 @@ import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterUtils; -import org.keycloak.adapters.FindFile; +import org.keycloak.util.FindFile; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; diff --git a/integration/wildfly-extensions/pom.xml b/integration/wildfly-extensions/pom.xml index 201db70505..bb575aa679 100755 --- a/integration/wildfly-extensions/pom.xml +++ b/integration/wildfly-extensions/pom.xml @@ -35,6 +35,11 @@ wildfly-controller provided + + org.keycloak + keycloak-core + provided + org.keycloak keycloak-model-api diff --git a/pom.xml b/pom.xml index 0f15f1266f..2c97592f62 100755 --- a/pom.xml +++ b/pom.xml @@ -557,6 +557,11 @@ keycloak-connections-mongo ${project.version} + + org.keycloak + keycloak-connections-http-client + ${project.version} + org.keycloak keycloak-connections-mongo-update diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java index 59977e61d5..49aed7ac4f 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java @@ -3,7 +3,6 @@ package org.keycloak.proxy; import io.undertow.Undertow; import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.AuthenticationMode; -import io.undertow.security.handlers.AuthenticationCallHandler; import io.undertow.security.handlers.AuthenticationMechanismsHandler; import io.undertow.security.handlers.SecurityInitialHandler; import io.undertow.security.idm.Account; @@ -24,7 +23,7 @@ import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.jboss.logging.Logger; import org.keycloak.adapters.AdapterDeploymentContext; -import org.keycloak.adapters.FindFile; +import org.keycloak.util.FindFile; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.NodesRegistrationManagement; @@ -35,7 +34,6 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement; import org.keycloak.enums.SslRequired; import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.util.CertificateUtils; -import org.keycloak.util.PemUtils; import org.keycloak.util.SystemPropertiesJsonParserFactory; import org.xnio.Option; diff --git a/saml/saml-protocol/pom.xml b/saml/saml-protocol/pom.xml index 702be9d783..8c2e43a462 100755 --- a/saml/saml-protocol/pom.xml +++ b/saml/saml-protocol/pom.xml @@ -33,6 +33,11 @@ keycloak-core provided + + org.keycloak + keycloak-connections-http-client + provided + org.keycloak keycloak-services diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index f650b64270..9db48d10e6 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -1,9 +1,14 @@ package org.keycloak.protocol.saml; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.message.BasicNameValuePair; import org.jboss.logging.Logger; -import org.jboss.resteasy.client.ClientRequest; -import org.jboss.resteasy.client.ClientResponse; -import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; +import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.protocol.ResponseType; @@ -37,7 +42,9 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.io.IOException; +import java.io.InputStream; import java.security.PublicKey; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -464,6 +471,11 @@ public class SamlProtocol implements LoginProtocol { public Response finishLogout(UserSessionModel userSession) { logger.debug("finishLogout"); String logoutBindingUri = userSession.getNote(SAML_LOGOUT_BINDING_URI); + if (logoutBindingUri == null) { + logger.error("Can't finish SAML logout as there is no logout binding set"); + return ErrorPage.error(session, Messages.FAILED_LOGOUT); + + } String logoutRelayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE); SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID)); @@ -515,35 +527,38 @@ public class SamlProtocol implements LoginProtocol { } - ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); - - - try { - ClientRequest request = executor.createRequest(logoutUrl); - request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString); - request.formParameter("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT"); // for Picketlink adapter, todo remove this - ClientResponse response = null; + HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient(); + for (int i = 0; i < 2; i++) { // follow redirects once try { - response = request.post(); - response.releaseConnection(); - // Undertow will redirect root urls not ending in "/" to root url + "/". Test for this weird behavior - if (response.getStatus() == 302 && !logoutUrl.endsWith("/")) { - String redirect = (String)response.getHeaders().getFirst(HttpHeaders.LOCATION); - String withSlash = logoutUrl + "/"; - if (withSlash.equals(redirect)) { - request = executor.createRequest(withSlash); - request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString); - request.formParameter("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT"); // for Picketlink adapter, todo remove this - response = request.post(); - response.releaseConnection(); + List formparams = new ArrayList(); + formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString)); + formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink todo remove this + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + HttpPost post = new HttpPost(logoutUrl); + post.setEntity(form); + HttpResponse response = httpClient.execute(post); + try { + int status = response.getStatusLine().getStatusCode(); + if (status == 302 && !logoutUrl.endsWith("/")) { + String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); + String withSlash = logoutUrl + "/"; + if (withSlash.equals(redirect)) { + logoutUrl = withSlash; + continue; + } } + } finally { + HttpEntity entity = response.getEntity(); + if (entity != null) { + InputStream is = entity.getContent(); + if (is != null) is.close(); + } + } - } catch (Exception e) { + } catch (IOException e) { logger.warn("failed to send saml logout", e); } - - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); + break; } } diff --git a/services/pom.xml b/services/pom.xml index 2fa148d848..3e85216111 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -34,6 +34,11 @@ keycloak-core-jaxrs provided + + org.keycloak + keycloak-connections-http-client + provided + org.keycloak keycloak-forms-common-freemarker diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 06eefe0f53..b7c7381aa7 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -22,7 +22,6 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; -import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; @@ -166,14 +165,7 @@ public class OIDCLoginProtocol implements LoginProtocol { public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { if (!(clientSession.getClient() instanceof ClientModel)) return; ClientModel app = clientSession.getClient(); - // TODO: Probably non-effective to build executor every time from scratch. Should be likely shared for whole OIDCLoginProtocolFactory - ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); - - try { - new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } + new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession); } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index ac64cbc5b0..dc0b6a4e47 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -1,11 +1,8 @@ package org.keycloak.services.managers; -import org.apache.http.client.HttpClient; import org.jboss.logging.Logger; -import org.jboss.resteasy.client.ClientRequest; -import org.jboss.resteasy.client.ClientResponse; -import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; import org.keycloak.TokenIdGenerator; +import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.constants.AdapterConstants; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; @@ -18,15 +15,14 @@ import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction; -import org.keycloak.services.util.HttpClientBuilder; import org.keycloak.services.util.ResolveRelative; import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.Time; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; +import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -44,11 +40,10 @@ public class ResourceAdminManager { protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); private static final String CLIENT_SESSION_HOST_PROPERTY = "${application.session.host}"; - public static ApacheHttpClient4Executor createExecutor() { - HttpClient client = new HttpClientBuilder() - .disableTrustManager() // todo fix this, should have a trust manager or a good default - .build(); - return new ApacheHttpClient4Executor(client); + private KeycloakSession session; + + public ResourceAdminManager(KeycloakSession session) { + this.session = session; } public static String resolveUri(URI requestUri, String uri) { @@ -101,23 +96,17 @@ public class ResourceAdminManager { } protected void logoutUserSessions(URI requestUri, RealmModel realm, List userSessions) { - ApacheHttpClient4Executor executor = createExecutor(); + // Map from "app" to clientSessions for this app + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + for (UserSessionModel userSession : userSessions) { + putClientSessions(clientSessions, userSession); + } - try { - // Map from "app" to clientSessions for this app - MultivaluedHashMap clientSessions = new MultivaluedHashMap(); - for (UserSessionModel userSession : userSessions) { - putClientSessions(clientSessions, userSession); - } + logger.debugv("logging out {0} resources ", clientSessions.size()); + //logger.infov("logging out resources: {0}", clientSessions); - logger.debugv("logging out {0} resources ", clientSessions.size()); - //logger.infov("logging out resources: {0}", clientSessions); - - for (Map.Entry> entry : clientSessions.entrySet()) { - logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor); - } - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); + for (Map.Entry> entry : clientSessions.entrySet()) { + logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue()); } } @@ -128,32 +117,25 @@ public class ResourceAdminManager { } } - public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user, KeycloakSession session) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - List userSessions = session.sessions().getUserSessions(realm, user); - List ourAppClientSessions = null; - if (userSessions != null) { - MultivaluedHashMap clientSessions = new MultivaluedHashMap(); - for (UserSessionModel userSession : userSessions) { - putClientSessions(clientSessions, userSession); - } - ourAppClientSessions = clientSessions.get(resource); + public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) { + List userSessions = session.sessions().getUserSessions(realm, user); + List ourAppClientSessions = null; + if (userSessions != null) { + MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + for (UserSessionModel userSession : userSessions) { + putClientSessions(clientSessions, userSession); } - - logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); + ourAppClientSessions = clientSessions.get(resource); } + logoutClientSessions(requestUri, realm, resource, ourAppClientSessions); } - public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) { - return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client); + public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession) { + return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession)); } - protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List clientSessions, ApacheHttpClient4Executor client) { + protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List clientSessions) { String managementUrl = getManagementUrl(requestUri, resource); if (managementUrl != null) { @@ -184,7 +166,7 @@ public class ResourceAdminManager { String host = entry.getKey(); List sessionIds = entry.getValue(); String currentHostMgmtUrl = managementUrl.replace(CLIENT_SESSION_HOST_PROPERTY, host); - allPassed = sendLogoutRequest(realm, resource, sessionIds, userSessions, client, 0, currentHostMgmtUrl) && allPassed; + allPassed = sendLogoutRequest(realm, resource, sessionIds, userSessions, 0, currentHostMgmtUrl) && allPassed; } return allPassed; @@ -195,7 +177,7 @@ public class ResourceAdminManager { allSessionIds.addAll(currentIds); } - return sendLogoutRequest(realm, resource, allSessionIds, userSessions, client, 0, managementUrl); + return sendLogoutRequest(realm, resource, allSessionIds, userSessions, 0, managementUrl); } } else { logger.debugv("Can't logout {0}: no management url", resource.getClientId()); @@ -206,36 +188,25 @@ public class ResourceAdminManager { // Methods for logout all public GlobalRequestResult logoutAll(URI requestUri, RealmModel realm) { - ApacheHttpClient4Executor executor = createExecutor(); + realm.setNotBefore(Time.currentTime()); + List resources = realm.getClients(); + logger.debugv("logging out {0} resources ", resources.size()); - try { - realm.setNotBefore(Time.currentTime()); - List resources = realm.getClients(); - logger.debugv("logging out {0} resources ", resources.size()); - - GlobalRequestResult finalResult = new GlobalRequestResult(); - for (ClientModel resource : resources) { - GlobalRequestResult currentResult = logoutClient(requestUri, realm, resource, executor, realm.getNotBefore()); - finalResult.addAll(currentResult); - } - return finalResult; - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); + GlobalRequestResult finalResult = new GlobalRequestResult(); + for (ClientModel resource : resources) { + GlobalRequestResult currentResult = logoutClient(requestUri, realm, resource, realm.getNotBefore()); + finalResult.addAll(currentResult); } + return finalResult; } public GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource) { - ApacheHttpClient4Executor executor = createExecutor(); - try { - resource.setNotBefore(Time.currentTime()); - return logoutClient(requestUri, realm, resource, executor, resource.getNotBefore()); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } + resource.setNotBefore(Time.currentTime()); + return logoutClient(requestUri, realm, resource, resource.getNotBefore()); } - protected GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource, ApacheHttpClient4Executor executor, int notBefore) { + protected GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource, int notBefore) { List mgmtUrls = getAllManagementUrls(requestUri, resource); if (mgmtUrls.isEmpty()) { logger.debug("No management URL or no registered cluster nodes for the client " + resource.getClientId()); @@ -247,7 +218,7 @@ public class ResourceAdminManager { // Propagate this to all hosts GlobalRequestResult result = new GlobalRequestResult(); for (String mgmtUrl : mgmtUrls) { - if (sendLogoutRequest(realm, resource, null, null, executor, notBefore, mgmtUrl)) { + if (sendLogoutRequest(realm, resource, null, null, notBefore, mgmtUrl)) { result.addSuccessRequest(mgmtUrl); } else { result.addFailedRequest(mgmtUrl); @@ -256,54 +227,37 @@ public class ResourceAdminManager { return result; } - protected boolean sendLogoutRequest(RealmModel realm, ClientModel resource, List adapterSessionIds, List userSessions, ApacheHttpClient4Executor client, int notBefore, String managementUrl) { + protected boolean sendLogoutRequest(RealmModel realm, ClientModel resource, List adapterSessionIds, List userSessions, int notBefore, String managementUrl) { LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), adapterSessionIds, notBefore, userSessions); String token = new TokenManager().encodeToken(realm, adminAction); if (logger.isDebugEnabled()) logger.debugv("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getClientId(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build().toString()); - ClientResponse response; + URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build(); try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); - } catch (Exception e) { - logger.warn("Logout for client '" + resource.getClientId() + "' failed", e); - return false; - } - try { - boolean success = response.getStatus() == 204 || response.getStatus() == 200; + int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token); + boolean success = status == 204 || status == 200; logger.debugf("logout success for %s: %s", managementUrl, success); return success; - } finally { - response.releaseConnection(); + } catch (IOException e) { + logger.warn("Logout for client '" + resource.getClientId() + "' failed", e); + return false; } } public GlobalRequestResult pushRealmRevocationPolicy(URI requestUri, RealmModel realm) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - GlobalRequestResult finalResult = new GlobalRequestResult(); - for (ClientModel client : realm.getClients()) { - GlobalRequestResult currentResult = pushRevocationPolicy(requestUri, realm, client, realm.getNotBefore(), executor); - finalResult.addAll(currentResult); - } - return finalResult; - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); + GlobalRequestResult finalResult = new GlobalRequestResult(); + for (ClientModel client : realm.getClients()) { + GlobalRequestResult currentResult = pushRevocationPolicy(requestUri, realm, client, realm.getNotBefore()); + finalResult.addAll(currentResult); } + return finalResult; } public GlobalRequestResult pushClientRevocationPolicy(URI requestUri, RealmModel realm, ClientModel client) { - ApacheHttpClient4Executor executor = createExecutor(); - - try { - return pushRevocationPolicy(requestUri, realm, client, client.getNotBefore(), executor); - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); - } + return pushRevocationPolicy(requestUri, realm, client, client.getNotBefore()); } - protected GlobalRequestResult pushRevocationPolicy(URI requestUri, RealmModel realm, ClientModel resource, int notBefore, ApacheHttpClient4Executor executor) { + protected GlobalRequestResult pushRevocationPolicy(URI requestUri, RealmModel realm, ClientModel resource, int notBefore) { List mgmtUrls = getAllManagementUrls(requestUri, resource); if (mgmtUrls.isEmpty()) { logger.debugf("No management URL or no registered cluster nodes for the client %s", resource.getClientId()); @@ -315,7 +269,7 @@ public class ResourceAdminManager { // Propagate this to all hosts GlobalRequestResult result = new GlobalRequestResult(); for (String mgmtUrl : mgmtUrls) { - if (sendPushRevocationPolicyRequest(realm, resource, notBefore, executor, mgmtUrl)) { + if (sendPushRevocationPolicyRequest(realm, resource, notBefore, mgmtUrl)) { result.addSuccessRequest(mgmtUrl); } else { result.addFailedRequest(mgmtUrl); @@ -324,24 +278,19 @@ public class ResourceAdminManager { return result; } - protected boolean sendPushRevocationPolicyRequest(RealmModel realm, ClientModel resource, int notBefore, ApacheHttpClient4Executor client, String managementUrl) { + protected boolean sendPushRevocationPolicyRequest(RealmModel realm, ClientModel resource, int notBefore, String managementUrl) { PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), notBefore); String token = new TokenManager().encodeToken(realm, adminAction); logger.infov("pushRevocation resource: {0} url: {1}", resource.getClientId(), managementUrl); - ClientRequest request = client.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build().toString()); - ClientResponse response; + URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build(); try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); - } catch (Exception e) { - logger.warn("Failed to send revocation request", e); - return false; - } - try { - boolean success = response.getStatus() == 204 || response.getStatus() == 200; + int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token); + boolean success = status == 204 || status == 200; logger.debugf("pushRevocation success for %s: %s", managementUrl, success); return success; - } finally { - response.releaseConnection(); + } catch (IOException e) { + logger.warn("Failed to send revocation request", e); + return false; } } @@ -352,45 +301,35 @@ public class ResourceAdminManager { return new GlobalRequestResult(); } - ApacheHttpClient4Executor executor = createExecutor(); - try { - if (logger.isDebugEnabled()) logger.debug("Sending test nodes availability: " + mgmtUrls); + if (logger.isDebugEnabled()) logger.debug("Sending test nodes availability: " + mgmtUrls); - // Propagate this to all hosts - GlobalRequestResult result = new GlobalRequestResult(); - for (String mgmtUrl : mgmtUrls) { - if (sendTestNodeAvailabilityRequest(realm, client, executor, mgmtUrl)) { - result.addSuccessRequest(mgmtUrl); - } else { - result.addFailedRequest(mgmtUrl); - } + // Propagate this to all hosts + GlobalRequestResult result = new GlobalRequestResult(); + for (String mgmtUrl : mgmtUrls) { + if (sendTestNodeAvailabilityRequest(realm, client, mgmtUrl)) { + result.addSuccessRequest(mgmtUrl); + } else { + result.addFailedRequest(mgmtUrl); } - return result; - } finally { - executor.getHttpClient().getConnectionManager().shutdown(); } + return result; } - protected boolean sendTestNodeAvailabilityRequest(RealmModel realm, ClientModel client, ApacheHttpClient4Executor httpClient, String managementUrl) { + protected boolean sendTestNodeAvailabilityRequest(RealmModel realm, ClientModel client, String managementUrl) { TestAvailabilityAction adminAction = new TestAvailabilityAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, client.getClientId()); String token = new TokenManager().encodeToken(realm, adminAction); logger.debugv("testNodes availability resource: {0} url: {1}", client.getClientId(), managementUrl); - ClientRequest request = httpClient.createRequest(UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_TEST_AVAILABLE).build().toString()); - ClientResponse response; + URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_TEST_AVAILABLE).build(); try { - response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); - } catch (Exception e) { + int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token); + boolean success = status == 204 || status == 200; + logger.debugf("testAvailability success for %s: %s", managementUrl, success); + return success; + } catch (IOException e) { logger.warn("Availability test failed for uri '" + managementUrl + "'", e); return false; } - try { - boolean success = response.getStatus() == 204 || response.getStatus() == 200; - logger.debugf("testAvailability success for %s: %s", managementUrl, success); - return success; - } finally { - response.releaseConnection(); - } - } + } } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index f142e9ac82..8aaa06edb1 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -173,4 +173,6 @@ public class Messages { public static final String INVALID_PARAMETER = "invalidParameterMessage"; public static final String IDENTITY_PROVIDER_LOGIN_FAILURE = "identityProviderLoginFailure"; + + public static final String FAILED_LOGOUT = "failedLogout"; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 98eb743a4f..f769101903 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -289,7 +289,7 @@ public class ClientResource { @POST public GlobalRequestResult pushRevocation() { auth.requireManage(); - return new ResourceAdminManager().pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client); + return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client); } /** @@ -341,7 +341,7 @@ public class ClientResource { @POST public GlobalRequestResult logoutAll() { auth.requireManage(); - return new ResourceAdminManager().logoutClient(uriInfo.getRequestUri(), realm, client); + return new ResourceAdminManager(session).logoutClient(uriInfo.getRequestUri(), realm, client); } /** @@ -356,7 +356,7 @@ public class ClientResource { if (user == null) { throw new NotFoundException("User not found"); } - new ResourceAdminManager().logoutUserFromClient(uriInfo.getRequestUri(), realm, client, user, session); + new ResourceAdminManager(session).logoutUserFromClient(uriInfo.getRequestUri(), realm, client, user); } /** @@ -410,7 +410,7 @@ public class ClientResource { auth.requireManage(); logger.debug("Test availability of cluster nodes"); - return new ResourceAdminManager().testNodesAvailability(uriInfo.getRequestUri(), realm, client); + return new ResourceAdminManager(session).testNodesAvailability(uriInfo.getRequestUri(), realm, client); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index 34cdc784b0..ec33725c22 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -1,13 +1,13 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; -import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -16,7 +16,6 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.ErrorResponse; import org.keycloak.social.SocialIdentityProvider; @@ -93,16 +92,18 @@ public class IdentityProvidersResource { String providerId = data.get("providerId").toString(); String from = data.get("fromUrl").toString(); - ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); - InputStream inputStream = null; + InputStream inputStream = session.getProvider(HttpClientProvider.class).get(from); try { - inputStream = executor.createRequest(from).getTarget(InputStream.class); - } catch (Exception e) { - throw new RuntimeException(e); + IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); + Map config; + config = providerFactory.parseConfig(inputStream); + return config; + } finally { + try { + inputStream.close(); + } catch (IOException e) { + } } - IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); - Map config = providerFactory.parseConfig(inputStream); - return config; } @GET diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 129ee5ef50..b0d171b53c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -254,7 +254,7 @@ public class RealmAdminResource { @POST public GlobalRequestResult pushRevocation() { auth.requireManage(); - return new ResourceAdminManager().pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm); + return new ResourceAdminManager(session).pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm); } /** @@ -266,7 +266,7 @@ public class RealmAdminResource { @POST public GlobalRequestResult logoutAll() { session.sessions().removeUserSessions(realm); - return new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm); + return new ResourceAdminManager(session).logoutAll(uriInfo.getRequestUri(), realm); } /** @@ -364,7 +364,6 @@ public class RealmAdminResource { * Query events. Returns all events, or will query based on URL query parameters listed here * * @param client app or oauth client name - * @param types type type * @param user user id * @param ipAddress * @param firstResult diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index 5c21db1a4a..694b5a9e25 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -359,7 +359,7 @@ public class AdapterTestStrategy extends ExternalResource { realm = session.realms().getRealmByName("demo"); // need to cleanup so other tests don't fail, so invalidate http sessions on remote clients. UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm); - new ResourceAdminManager().logoutUser(null, realm, user, session); + new ResourceAdminManager(session).logoutUser(null, realm, user, session); realm.setSsoSessionIdleTimeout(originalIdle); session.getTransaction().commit(); session.close(); diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index e2ebf541b3..a3f508a86a 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -67,6 +67,13 @@ "interval": 900 }, + "connectionsHttpClient": { + "default": { + "disable-trust-manager": true + } + }, + + "connectionsJpa": { "default": { "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",