Merge pull request #1199 from patriot1burke/master

apache http client fixes
This commit is contained in:
Bill Burke 2015-04-29 21:59:50 -04:00
commit 761be66362
40 changed files with 893 additions and 204 deletions

36
connections/http-client/pom.xml Executable file
View file

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

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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) {
}
}

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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. <i>NOTE</i> 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();
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface HttpClientFactory extends ProviderFactory<HttpClientProvider> {
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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;
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class HttpClientSpi implements Spi {
@Override
public String getName() {
return "connectionsHttpClient";
}
@Override
public Class<? extends Provider> getProviderClass() {
return HttpClientProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return HttpClientFactory.class;
}
}

View file

@ -0,0 +1 @@
org.keycloak.connections.httpclient.DefaultHttpClientFactory

View file

@ -0,0 +1 @@
org.keycloak.connections.httpclient.HttpClientSpi

View file

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

View file

@ -1,4 +1,4 @@
package org.keycloak.adapters; package org.keycloak.util;
import org.keycloak.constants.GenericConstants; import org.keycloak.constants.GenericConstants;

View file

@ -135,6 +135,11 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId> <artifactId>keycloak-export-import-single-file</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -169,6 +169,10 @@
<!-- server all dependencies --> <!-- server all dependencies -->
<module-def name="org.keycloak.keycloak-connections-http-client">
<maven-resource group="org.keycloak" artifact="keycloak-connections-http-client"/>
</module-def>
<module-def name="org.keycloak.keycloak-connections-jpa"> <module-def name="org.keycloak.keycloak-connections-jpa">
<maven-resource group="org.keycloak" artifact="keycloak-connections-jpa"/> <maven-resource group="org.keycloak" artifact="keycloak-connections-jpa"/>
</module-def> </module-def>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-connections-http-client">
<resources>
<!-- Insert resources here -->
</resources>
<exports>
<include path="META-INF/**"/>
</exports>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
<module name="org.apache.httpcomponents" slot="4.3" />
</dependencies>
</module>

View file

@ -21,6 +21,7 @@
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/> <module name="org.jboss.resteasy.resteasy-jaxrs"/>
<module name="org.jboss.resteasy.resteasy-multipart-provider"/> <module name="org.jboss.resteasy.resteasy-multipart-provider"/>
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
<module name="javax.api"/> <module name="javax.api"/>
</dependencies> </dependencies>

View file

@ -14,6 +14,7 @@
<module name="org.keycloak.keycloak-broker-saml" services="import"/> <module name="org.keycloak.keycloak-broker-saml" services="import"/>
<module name="org.keycloak.keycloak-connections-infinispan" services="import"/> <module name="org.keycloak.keycloak-connections-infinispan" services="import"/>
<module name="org.keycloak.keycloak-connections-jpa" services="import"/> <module name="org.keycloak.keycloak-connections-jpa" services="import"/>
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>

View file

@ -63,6 +63,12 @@
"interval": 900 "interval": 900
}, },
"connectionsHttpClient": {
"default": {
"disable-trust-manager": true
}
},
"connectionsJpa": { "connectionsJpa": {
"default": { "default": {
"dataSource": "java:jboss/datasources/KeycloakDS", "dataSource": "java:jboss/datasources/KeycloakDS",

View file

@ -395,6 +395,150 @@ All configuration options are optional. Default value for directory is <literal>
</para> </para>
</section> </section>
<section>
<title>Outgoing Server HTTP Requests</title>
<para>
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
<literal>standalone/configuration/keycloak-server.json</literal>.
By default the setting is like this:
<programlisting><![CDATA[
"connectionsHttpClient": {
"default": {
"disable-trust-manager": true
}
},
]]></programlisting>
Possible configuration options are:
<variablelist>
<varlistentry>
<term>establish-connection-timeout-millis</term>
<listitem>
<para>
Timeout for establishing a socket connection.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>socket-timeout-millis</term>
<listitem>
<para>
If an outgoing request does not receive data for this amount of time, timeout the connection.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>connection-pool-size</term>
<listitem>
<para>
How many connections can be in the pool.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>max-pooled-per-route</term>
<listitem>
<para>
How many connections can be pooled per host.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>disable-trust-manager</term>
<listitem>
<para>
If true, HTTPS server certificates are not verified. If you set this to false, you must
configure a truststore.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>disable-cookies</term>
<listitem>
<para>
<literal>true</literal> by default. When set to true, this will disable any cookie
caching.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>hostname-verification-policy</term>
<listitem>
<para>
<literal>WILDCARD</literal> by default. For HTTPS requests, this verifies the hostname
of the server's certificate. <literal>ANY</literal> means that the hostname is not verified.
<literal>WILDCARD</literal> Allows wildcards in subdomain names i.e. *.foo.com.
<literal>STRICT</literal> CN must match hostname exactly.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore</term>
<listitem>
<para>
The value is the file path to a Java keystore file. If
you prefix the path with <literal>classpath:</literal>, then the truststore will be obtained
from the deployment's classpath instead.
HTTPS
requests need a way to verify the host of the server they are talking to. This is
what the trustore does. The keystore contains one or more trusted
host certificates or certificate authorities.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore-password</term>
<listitem>
<para>
Password for the truststore keystore.
This is
<emphasis>REQUIRED</emphasis>
if
<literal>truststore</literal>
is set.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>client-keystore</term>
<listitem>
<para>
This is the file path to a Java keystore file.
This keystore contains client certificate for two-way SSL.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>client-keystore-password</term>
<listitem>
<para>
Password for the client keystore.
This is
<emphasis>REQUIRED</emphasis>
if
<literal>client-keystore</literal>
is set.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>client-key-password</term>
<listitem>
<para>
<emphasis>Not supported yet, but we will support in future versions.</emphasis>
Password for the client's key.
This is
<emphasis>REQUIRED</emphasis>
if
<literal>client-keystore</literal>
is set.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
<section id="ssl_modes"> <section id="ssl_modes">
<title>SSL/HTTPS Requirement/Modes</title> <title>SSL/HTTPS Requirement/Modes</title>
<warning> <warning>

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>keycloak-examples-broker-parent</artifactId> <artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version> <version>1.2.0.RC1-SNAPSHOT</version>
</parent> </parent>

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>keycloak-examples-broker-parent</artifactId> <artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version> <version>1.2.0.RC1-SNAPSHOT</version>
</parent> </parent>

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>keycloak-examples-broker-parent</artifactId> <artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version> <version>1.2.0.RC1-SNAPSHOT</version>
</parent> </parent>

View file

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<artifactId>keycloak-examples-broker-parent</artifactId> <artifactId>keycloak-examples-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>1.2.0.RC1-SNAPSHOT</version> <version>1.2.0.RC1-SNAPSHOT</version>
</parent> </parent>

View file

@ -138,6 +138,7 @@ failedToProcessResponseMessage=Konnte Response nicht verarbeiten.
httpsRequiredMessage=HTTPS erforderlich. httpsRequiredMessage=HTTPS erforderlich.
realmNotEnabledMessage=Realm nicht aktiviert. realmNotEnabledMessage=Realm nicht aktiviert.
invalidRequestMessage=Ung\u00FCltiger Request. invalidRequestMessage=Ung\u00FCltiger Request.
failedLogout=Logout failed
unknownLoginRequesterMessage=Ung\u00FCltiger login requester unknownLoginRequesterMessage=Ung\u00FCltiger login requester
loginRequesterNotEnabledMessage=Login requester nicht aktiviert. loginRequesterNotEnabledMessage=Login requester nicht aktiviert.
bearerOnlyMessage=Bearer-only Applikationen k\u00F6nne sich nicht via Browser anmelden. bearerOnlyMessage=Bearer-only Applikationen k\u00F6nne sich nicht via Browser anmelden.

View file

@ -140,6 +140,7 @@ failedToProcessResponseMessage=Failed to process response
httpsRequiredMessage=HTTPS required httpsRequiredMessage=HTTPS required
realmNotEnabledMessage=Realm not enabled realmNotEnabledMessage=Realm not enabled
invalidRequestMessage=Invalid Request invalidRequestMessage=Invalid Request
failedLogout=Logout failed
unknownLoginRequesterMessage=Unknown login requester unknownLoginRequesterMessage=Unknown login requester
loginRequesterNotEnabledMessage=Login requester not enabled loginRequesterNotEnabledMessage=Login requester not enabled
bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login

View file

@ -135,6 +135,7 @@ failedToProcessResponseMessage=Fallimento nell''elaborazione della risposta
httpsRequiredMessage=HTTPS richiesto httpsRequiredMessage=HTTPS richiesto
realmNotEnabledMessage=Realm non abilitato realmNotEnabledMessage=Realm non abilitato
invalidRequestMessage=Richiesta non valida invalidRequestMessage=Richiesta non valida
failedLogout=Logout failed
unknownLoginRequesterMessage=Richiedente di Login non riconosciuto unknownLoginRequesterMessage=Richiedente di Login non riconosciuto
loginRequesterNotEnabledMessage=Richiedente di Login non abilitato loginRequesterNotEnabledMessage=Richiedente di Login non abilitato
bearerOnlyMessage=Alle applicazioni di tipo Bearer-only non e'' consentito di effettuare il login tramite browser bearerOnlyMessage=Alle applicazioni di tipo Bearer-only non e'' consentito di effettuare il login tramite browser

View file

@ -135,6 +135,7 @@ failedToProcessResponseMessage=Falha ao processar a resposta
httpsRequiredMessage=HTTPS requerido httpsRequiredMessage=HTTPS requerido
realmNotEnabledMessage=Realm desativado realmNotEnabledMessage=Realm desativado
invalidRequestMessage=Pedido inv\u00E1lido invalidRequestMessage=Pedido inv\u00E1lido
failedLogout=Logout failed
unknownLoginRequesterMessage=Solicitante de login desconhecido unknownLoginRequesterMessage=Solicitante de login desconhecido
loginRequesterNotEnabledMessage=Solicitante de login desativado loginRequesterNotEnabledMessage=Solicitante de login desativado
bearerOnlyMessage=Aplica\u00E7\u00F5es somente ao portador n\u00E3o tem permiss\u00E3o para iniciar o login pelo navegador bearerOnlyMessage=Aplica\u00E7\u00F5es somente ao portador n\u00E3o tem permiss\u00E3o para iniciar o login pelo navegador

View file

@ -24,7 +24,7 @@ import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException; import org.keycloak.VerificationException;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.FindFile; import org.keycloak.util.FindFile;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;

View file

@ -35,6 +35,11 @@
<artifactId>wildfly-controller</artifactId> <artifactId>wildfly-controller</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId> <artifactId>keycloak-model-api</artifactId>

View file

@ -557,6 +557,11 @@
<artifactId>keycloak-connections-mongo</artifactId> <artifactId>keycloak-connections-mongo</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-mongo-update</artifactId> <artifactId>keycloak-connections-mongo-update</artifactId>

View file

@ -3,7 +3,6 @@ package org.keycloak.proxy;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.AuthenticationMode; import io.undertow.security.api.AuthenticationMode;
import io.undertow.security.handlers.AuthenticationCallHandler;
import io.undertow.security.handlers.AuthenticationMechanismsHandler; import io.undertow.security.handlers.AuthenticationMechanismsHandler;
import io.undertow.security.handlers.SecurityInitialHandler; import io.undertow.security.handlers.SecurityInitialHandler;
import io.undertow.security.idm.Account; 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.codehaus.jackson.map.annotate.JsonSerialize;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.FindFile; import org.keycloak.util.FindFile;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.NodesRegistrationManagement;
@ -35,7 +34,6 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.enums.SslRequired; import org.keycloak.enums.SslRequired;
import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.CertificateUtils; import org.keycloak.util.CertificateUtils;
import org.keycloak.util.PemUtils;
import org.keycloak.util.SystemPropertiesJsonParserFactory; import org.keycloak.util.SystemPropertiesJsonParserFactory;
import org.xnio.Option; import org.xnio.Option;

View file

@ -33,6 +33,11 @@
<artifactId>keycloak-core</artifactId> <artifactId>keycloak-core</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId> <artifactId>keycloak-services</artifactId>

View file

@ -1,9 +1,14 @@
package org.keycloak.protocol.saml; 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.logging.Logger;
import org.jboss.resteasy.client.ClientRequest; import org.keycloak.connections.httpclient.HttpClientProvider;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.protocol.ResponseType; 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.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -464,6 +471,11 @@ public class SamlProtocol implements LoginProtocol {
public Response finishLogout(UserSessionModel userSession) { public Response finishLogout(UserSessionModel userSession) {
logger.debug("finishLogout"); logger.debug("finishLogout");
String logoutBindingUri = userSession.getNote(SAML_LOGOUT_BINDING_URI); 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); String logoutRelayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE);
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID)); builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID));
@ -515,35 +527,38 @@ public class SamlProtocol implements LoginProtocol {
} }
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
for (int i = 0; i < 2; i++) { // follow redirects once
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;
try { try {
response = request.post(); List<NameValuePair> formparams = new ArrayList<NameValuePair>();
response.releaseConnection(); formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
// Undertow will redirect root urls not ending in "/" to root url + "/". Test for this weird behavior formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink todo remove this
if (response.getStatus() == 302 && !logoutUrl.endsWith("/")) { UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
String redirect = (String)response.getHeaders().getFirst(HttpHeaders.LOCATION); HttpPost post = new HttpPost(logoutUrl);
String withSlash = logoutUrl + "/"; post.setEntity(form);
if (withSlash.equals(redirect)) { HttpResponse response = httpClient.execute(post);
request = executor.createRequest(withSlash); try {
request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString); int status = response.getStatusLine().getStatusCode();
request.formParameter("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT"); // for Picketlink adapter, todo remove this if (status == 302 && !logoutUrl.endsWith("/")) {
response = request.post(); String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
response.releaseConnection(); 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); logger.warn("failed to send saml logout", e);
} }
break;
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
} }
} }

View file

@ -34,6 +34,11 @@
<artifactId>keycloak-core-jaxrs</artifactId> <artifactId>keycloak-core-jaxrs</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-forms-common-freemarker</artifactId> <artifactId>keycloak-forms-common-freemarker</artifactId>

View file

@ -22,7 +22,6 @@
package org.keycloak.protocol.oidc; package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -166,14 +165,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
if (!(clientSession.getClient() instanceof ClientModel)) return; if (!(clientSession.getClient() instanceof ClientModel)) return;
ClientModel app = clientSession.getClient(); ClientModel app = clientSession.getClient();
// TODO: Probably non-effective to build executor every time from scratch. Should be likely shared for whole OIDCLoginProtocolFactory new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession);
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
try {
new ResourceAdminManager().logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession, executor);
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
} }
@Override @Override

View file

@ -1,11 +1,8 @@
package org.keycloak.services.managers; package org.keycloak.services.managers;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger; 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.TokenIdGenerator;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; 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.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction; import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.TestAvailabilityAction; import org.keycloak.representations.adapters.action.TestAvailabilityAction;
import org.keycloak.services.util.HttpClientBuilder;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.util.KeycloakUriBuilder; import org.keycloak.util.KeycloakUriBuilder;
import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.MultivaluedHashMap;
import org.keycloak.util.StringPropertyReplacer; import org.keycloak.util.StringPropertyReplacer;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -44,11 +40,10 @@ public class ResourceAdminManager {
protected static Logger logger = Logger.getLogger(ResourceAdminManager.class); protected static Logger logger = Logger.getLogger(ResourceAdminManager.class);
private static final String CLIENT_SESSION_HOST_PROPERTY = "${application.session.host}"; private static final String CLIENT_SESSION_HOST_PROPERTY = "${application.session.host}";
public static ApacheHttpClient4Executor createExecutor() { private KeycloakSession session;
HttpClient client = new HttpClientBuilder()
.disableTrustManager() // todo fix this, should have a trust manager or a good default public ResourceAdminManager(KeycloakSession session) {
.build(); this.session = session;
return new ApacheHttpClient4Executor(client);
} }
public static String resolveUri(URI requestUri, String uri) { public static String resolveUri(URI requestUri, String uri) {
@ -101,23 +96,17 @@ public class ResourceAdminManager {
} }
protected void logoutUserSessions(URI requestUri, RealmModel realm, List<UserSessionModel> userSessions) { protected void logoutUserSessions(URI requestUri, RealmModel realm, List<UserSessionModel> userSessions) {
ApacheHttpClient4Executor executor = createExecutor(); // Map from "app" to clientSessions for this app
MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession);
}
try { logger.debugv("logging out {0} resources ", clientSessions.size());
// Map from "app" to clientSessions for this app //logger.infov("logging out resources: {0}", clientSessions);
MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession);
}
logger.debugv("logging out {0} resources ", clientSessions.size()); for (Map.Entry<ClientModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
//logger.infov("logging out resources: {0}", clientSessions); logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue());
for (Map.Entry<ClientModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue(), executor);
}
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
} }
} }
@ -128,32 +117,25 @@ public class ResourceAdminManager {
} }
} }
public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user, KeycloakSession session) { public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) {
ApacheHttpClient4Executor executor = createExecutor(); List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
List<ClientSessionModel> ourAppClientSessions = null;
try { if (userSessions != null) {
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user); MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
List<ClientSessionModel> ourAppClientSessions = null; for (UserSessionModel userSession : userSessions) {
if (userSessions != null) { putClientSessions(clientSessions, userSession);
MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession);
}
ourAppClientSessions = clientSessions.get(resource);
} }
ourAppClientSessions = clientSessions.get(resource);
logoutClientSessions(requestUri, realm, resource, ourAppClientSessions, executor);
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
} }
logoutClientSessions(requestUri, realm, resource, ourAppClientSessions);
} }
public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession, ApacheHttpClient4Executor client) { public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession) {
return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession), client); return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession));
} }
protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List<ClientSessionModel> clientSessions, ApacheHttpClient4Executor client) { protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List<ClientSessionModel> clientSessions) {
String managementUrl = getManagementUrl(requestUri, resource); String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) { if (managementUrl != null) {
@ -184,7 +166,7 @@ public class ResourceAdminManager {
String host = entry.getKey(); String host = entry.getKey();
List<String> sessionIds = entry.getValue(); List<String> sessionIds = entry.getValue();
String currentHostMgmtUrl = managementUrl.replace(CLIENT_SESSION_HOST_PROPERTY, host); 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; return allPassed;
@ -195,7 +177,7 @@ public class ResourceAdminManager {
allSessionIds.addAll(currentIds); allSessionIds.addAll(currentIds);
} }
return sendLogoutRequest(realm, resource, allSessionIds, userSessions, client, 0, managementUrl); return sendLogoutRequest(realm, resource, allSessionIds, userSessions, 0, managementUrl);
} }
} else { } else {
logger.debugv("Can't logout {0}: no management url", resource.getClientId()); logger.debugv("Can't logout {0}: no management url", resource.getClientId());
@ -206,36 +188,25 @@ public class ResourceAdminManager {
// Methods for logout all // Methods for logout all
public GlobalRequestResult logoutAll(URI requestUri, RealmModel realm) { public GlobalRequestResult logoutAll(URI requestUri, RealmModel realm) {
ApacheHttpClient4Executor executor = createExecutor(); realm.setNotBefore(Time.currentTime());
List<ClientModel> resources = realm.getClients();
logger.debugv("logging out {0} resources ", resources.size());
try { GlobalRequestResult finalResult = new GlobalRequestResult();
realm.setNotBefore(Time.currentTime()); for (ClientModel resource : resources) {
List<ClientModel> resources = realm.getClients(); GlobalRequestResult currentResult = logoutClient(requestUri, realm, resource, realm.getNotBefore());
logger.debugv("logging out {0} resources ", resources.size()); finalResult.addAll(currentResult);
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();
} }
return finalResult;
} }
public GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource) { public GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource) {
ApacheHttpClient4Executor executor = createExecutor(); resource.setNotBefore(Time.currentTime());
try { return logoutClient(requestUri, realm, resource, resource.getNotBefore());
resource.setNotBefore(Time.currentTime());
return logoutClient(requestUri, realm, resource, executor, resource.getNotBefore());
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
} }
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<String> mgmtUrls = getAllManagementUrls(requestUri, resource); List<String> mgmtUrls = getAllManagementUrls(requestUri, resource);
if (mgmtUrls.isEmpty()) { if (mgmtUrls.isEmpty()) {
logger.debug("No management URL or no registered cluster nodes for the client " + resource.getClientId()); 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 // Propagate this to all hosts
GlobalRequestResult result = new GlobalRequestResult(); GlobalRequestResult result = new GlobalRequestResult();
for (String mgmtUrl : mgmtUrls) { for (String mgmtUrl : mgmtUrls) {
if (sendLogoutRequest(realm, resource, null, null, executor, notBefore, mgmtUrl)) { if (sendLogoutRequest(realm, resource, null, null, notBefore, mgmtUrl)) {
result.addSuccessRequest(mgmtUrl); result.addSuccessRequest(mgmtUrl);
} else { } else {
result.addFailedRequest(mgmtUrl); result.addFailedRequest(mgmtUrl);
@ -256,54 +227,37 @@ public class ResourceAdminManager {
return result; return result;
} }
protected boolean sendLogoutRequest(RealmModel realm, ClientModel resource, List<String> adapterSessionIds, List<String> userSessions, ApacheHttpClient4Executor client, int notBefore, String managementUrl) { protected boolean sendLogoutRequest(RealmModel realm, ClientModel resource, List<String> adapterSessionIds, List<String> userSessions, int notBefore, String managementUrl) {
LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), adapterSessionIds, notBefore, userSessions); LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), adapterSessionIds, notBefore, userSessions);
String token = new TokenManager().encodeToken(realm, adminAction); String token = new TokenManager().encodeToken(realm, adminAction);
if (logger.isDebugEnabled()) logger.debugv("logout resource {0} url: {1} sessionIds: " + adapterSessionIds, resource.getClientId(), managementUrl); 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()); URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_LOGOUT).build();
ClientResponse response;
try { try {
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
} catch (Exception e) { boolean success = status == 204 || status == 200;
logger.warn("Logout for client '" + resource.getClientId() + "' failed", e);
return false;
}
try {
boolean success = response.getStatus() == 204 || response.getStatus() == 200;
logger.debugf("logout success for %s: %s", managementUrl, success); logger.debugf("logout success for %s: %s", managementUrl, success);
return success; return success;
} finally { } catch (IOException e) {
response.releaseConnection(); logger.warn("Logout for client '" + resource.getClientId() + "' failed", e);
return false;
} }
} }
public GlobalRequestResult pushRealmRevocationPolicy(URI requestUri, RealmModel realm) { public GlobalRequestResult pushRealmRevocationPolicy(URI requestUri, RealmModel realm) {
ApacheHttpClient4Executor executor = createExecutor(); GlobalRequestResult finalResult = new GlobalRequestResult();
for (ClientModel client : realm.getClients()) {
try { GlobalRequestResult currentResult = pushRevocationPolicy(requestUri, realm, client, realm.getNotBefore());
GlobalRequestResult finalResult = new GlobalRequestResult(); finalResult.addAll(currentResult);
for (ClientModel client : realm.getClients()) {
GlobalRequestResult currentResult = pushRevocationPolicy(requestUri, realm, client, realm.getNotBefore(), executor);
finalResult.addAll(currentResult);
}
return finalResult;
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
} }
return finalResult;
} }
public GlobalRequestResult pushClientRevocationPolicy(URI requestUri, RealmModel realm, ClientModel client) { public GlobalRequestResult pushClientRevocationPolicy(URI requestUri, RealmModel realm, ClientModel client) {
ApacheHttpClient4Executor executor = createExecutor(); return pushRevocationPolicy(requestUri, realm, client, client.getNotBefore());
try {
return pushRevocationPolicy(requestUri, realm, client, client.getNotBefore(), executor);
} finally {
executor.getHttpClient().getConnectionManager().shutdown();
}
} }
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<String> mgmtUrls = getAllManagementUrls(requestUri, resource); List<String> mgmtUrls = getAllManagementUrls(requestUri, resource);
if (mgmtUrls.isEmpty()) { if (mgmtUrls.isEmpty()) {
logger.debugf("No management URL or no registered cluster nodes for the client %s", resource.getClientId()); 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 // Propagate this to all hosts
GlobalRequestResult result = new GlobalRequestResult(); GlobalRequestResult result = new GlobalRequestResult();
for (String mgmtUrl : mgmtUrls) { for (String mgmtUrl : mgmtUrls) {
if (sendPushRevocationPolicyRequest(realm, resource, notBefore, executor, mgmtUrl)) { if (sendPushRevocationPolicyRequest(realm, resource, notBefore, mgmtUrl)) {
result.addSuccessRequest(mgmtUrl); result.addSuccessRequest(mgmtUrl);
} else { } else {
result.addFailedRequest(mgmtUrl); result.addFailedRequest(mgmtUrl);
@ -324,24 +278,19 @@ public class ResourceAdminManager {
return result; 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); PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getClientId(), notBefore);
String token = new TokenManager().encodeToken(realm, adminAction); String token = new TokenManager().encodeToken(realm, adminAction);
logger.infov("pushRevocation resource: {0} url: {1}", resource.getClientId(), managementUrl); 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()); URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).build();
ClientResponse response;
try { try {
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
} catch (Exception e) { boolean success = status == 204 || status == 200;
logger.warn("Failed to send revocation request", e);
return false;
}
try {
boolean success = response.getStatus() == 204 || response.getStatus() == 200;
logger.debugf("pushRevocation success for %s: %s", managementUrl, success); logger.debugf("pushRevocation success for %s: %s", managementUrl, success);
return success; return success;
} finally { } catch (IOException e) {
response.releaseConnection(); logger.warn("Failed to send revocation request", e);
return false;
} }
} }
@ -352,45 +301,35 @@ public class ResourceAdminManager {
return new GlobalRequestResult(); 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 // Propagate this to all hosts
GlobalRequestResult result = new GlobalRequestResult(); GlobalRequestResult result = new GlobalRequestResult();
for (String mgmtUrl : mgmtUrls) { for (String mgmtUrl : mgmtUrls) {
if (sendTestNodeAvailabilityRequest(realm, client, executor, mgmtUrl)) { if (sendTestNodeAvailabilityRequest(realm, client, mgmtUrl)) {
result.addSuccessRequest(mgmtUrl); result.addSuccessRequest(mgmtUrl);
} else { } else {
result.addFailedRequest(mgmtUrl); 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()); TestAvailabilityAction adminAction = new TestAvailabilityAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, client.getClientId());
String token = new TokenManager().encodeToken(realm, adminAction); String token = new TokenManager().encodeToken(realm, adminAction);
logger.debugv("testNodes availability resource: {0} url: {1}", client.getClientId(), managementUrl); 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()); URI target = UriBuilder.fromUri(managementUrl).path(AdapterConstants.K_TEST_AVAILABLE).build();
ClientResponse response;
try { try {
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(); int status = session.getProvider(HttpClientProvider.class).postText(target.toString(), token);
} catch (Exception e) { 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); logger.warn("Availability test failed for uri '" + managementUrl + "'", e);
return false; 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();
}
}
} }

View file

@ -173,4 +173,6 @@ public class Messages {
public static final String INVALID_PARAMETER = "invalidParameterMessage"; public static final String INVALID_PARAMETER = "invalidParameterMessage";
public static final String IDENTITY_PROVIDER_LOGIN_FAILURE = "identityProviderLoginFailure"; public static final String IDENTITY_PROVIDER_LOGIN_FAILURE = "identityProviderLoginFailure";
public static final String FAILED_LOGOUT = "failedLogout";
} }

View file

@ -289,7 +289,7 @@ public class ClientResource {
@POST @POST
public GlobalRequestResult pushRevocation() { public GlobalRequestResult pushRevocation() {
auth.requireManage(); 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 @POST
public GlobalRequestResult logoutAll() { public GlobalRequestResult logoutAll() {
auth.requireManage(); 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) { if (user == null) {
throw new NotFoundException("User not found"); 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(); auth.requireManage();
logger.debug("Test availability of cluster nodes"); logger.debug("Test availability of cluster nodes");
return new ResourceAdminManager().testNodesAvailability(uriInfo.getRequestUri(), realm, client); return new ResourceAdminManager(session).testNodesAvailability(uriInfo.getRequestUri(), realm, client);
} }
} }

View file

@ -1,13 +1,13 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.jboss.resteasy.annotations.cache.NoCache; 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.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -16,7 +16,6 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.social.SocialIdentityProvider; import org.keycloak.social.SocialIdentityProvider;
@ -93,16 +92,18 @@ public class IdentityProvidersResource {
String providerId = data.get("providerId").toString(); String providerId = data.get("providerId").toString();
String from = data.get("fromUrl").toString(); String from = data.get("fromUrl").toString();
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor(); InputStream inputStream = session.getProvider(HttpClientProvider.class).get(from);
InputStream inputStream = null;
try { try {
inputStream = executor.createRequest(from).getTarget(InputStream.class); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
} catch (Exception e) { Map<String, String> config;
throw new RuntimeException(e); config = providerFactory.parseConfig(inputStream);
return config;
} finally {
try {
inputStream.close();
} catch (IOException e) {
}
} }
IdentityProviderFactory providerFactory = getProviderFactorytById(providerId);
Map<String, String> config = providerFactory.parseConfig(inputStream);
return config;
} }
@GET @GET

View file

@ -254,7 +254,7 @@ public class RealmAdminResource {
@POST @POST
public GlobalRequestResult pushRevocation() { public GlobalRequestResult pushRevocation() {
auth.requireManage(); 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 @POST
public GlobalRequestResult logoutAll() { public GlobalRequestResult logoutAll() {
session.sessions().removeUserSessions(realm); 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 * Query events. Returns all events, or will query based on URL query parameters listed here
* *
* @param client app or oauth client name * @param client app or oauth client name
* @param types type type
* @param user user id * @param user user id
* @param ipAddress * @param ipAddress
* @param firstResult * @param firstResult

View file

@ -359,7 +359,7 @@ public class AdapterTestStrategy extends ExternalResource {
realm = session.realms().getRealmByName("demo"); realm = session.realms().getRealmByName("demo");
// need to cleanup so other tests don't fail, so invalidate http sessions on remote clients. // 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); 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); realm.setSsoSessionIdleTimeout(originalIdle);
session.getTransaction().commit(); session.getTransaction().commit();
session.close(); session.close();

View file

@ -67,6 +67,13 @@
"interval": 900 "interval": 900
}, },
"connectionsHttpClient": {
"default": {
"disable-trust-manager": true
}
},
"connectionsJpa": { "connectionsJpa": {
"default": { "default": {
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}", "url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test}",