Merge branch 'feature/acs23' into bugfix/500-on-404

This commit is contained in:
Brian Long 2025-01-09 14:10:39 -05:00
commit c25d2c94e6
6 changed files with 894 additions and 30 deletions

View File

@ -6,7 +6,7 @@ cache.${moduleId}.ssoToSessionCache.maxIdleSeconds=0
cache.${moduleId}.ssoToSessionCache.cluster.type=fully-distributed
cache.${moduleId}.ssoToSessionCache.backup-count=1
cache.${moduleId}.ssoToSessionCache.eviction-policy=LRU
cache.${moduleId}.ssoToSessionCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.ssoToSessionCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.ssoToSessionCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.ssoToSessionCache.clearable=false
@ -19,7 +19,7 @@ cache.${moduleId}.sessionToSsoCache.maxIdleSeconds=0
cache.${moduleId}.sessionToSsoCache.cluster.type=fully-distributed
cache.${moduleId}.sessionToSsoCache.backup-count=1
cache.${moduleId}.sessionToSsoCache.eviction-policy=LRU
cache.${moduleId}.sessionToSsoCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.sessionToSsoCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.sessionToSsoCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.sessionToSsoCache.clearable=false
@ -32,7 +32,7 @@ cache.${moduleId}.principalToSessionCache.maxIdleSeconds=0
cache.${moduleId}.principalToSessionCache.cluster.type=fully-distributed
cache.${moduleId}.principalToSessionCache.backup-count=1
cache.${moduleId}.principalToSessionCache.eviction-policy=LRU
cache.${moduleId}.principalToSessionCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.principalToSessionCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.principalToSessionCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.principalToSessionCache.clearable=false
@ -45,7 +45,7 @@ cache.${moduleId}.sessionToPrincipalCache.maxIdleSeconds=0
cache.${moduleId}.sessionToPrincipalCache.cluster.type=fully-distributed
cache.${moduleId}.sessionToPrincipalCache.backup-count=1
cache.${moduleId}.sessionToPrincipalCache.eviction-policy=LRU
cache.${moduleId}.sessionToPrincipalCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.sessionToPrincipalCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.sessionToPrincipalCache.readBackupData=false
# explicitly not clearable - should be cleared via Keycloak back-channel action
cache.${moduleId}.sessionToPrincipalCache.clearable=false
@ -58,7 +58,7 @@ cache.${moduleId}.ticketTokenCache.maxIdleSeconds=0
cache.${moduleId}.ticketTokenCache.cluster.type=fully-distributed
cache.${moduleId}.ticketTokenCache.backup-count=1
cache.${moduleId}.ticketTokenCache.eviction-policy=LRU
cache.${moduleId}.ticketTokenCache.merge-policy=com.hazelcast.map.merge.PutIfAbsentMapMergePolicy
cache.${moduleId}.ticketTokenCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy
cache.${moduleId}.ticketTokenCache.readBackupData=false
# dangerous to be cleared, as roles / claims can no longer be mapped
# would always be better to just invalidate the tickets themselves

View File

@ -4,21 +4,20 @@ import com.fasterxml.jackson.core.JsonParseException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.alfresco.util.ParameterCheck;
import org.apache.http.Header;
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.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
@ -38,6 +37,7 @@ import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.acosix.alfresco.keycloak.repo.util.NameValueMapAdapter;
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
/**
@ -284,20 +284,20 @@ public class AccessTokenClient
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl())
.path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm()));
final List<NameValuePair> formParams = new ArrayList<>();
final List<NameValuePair> formParams = new LinkedList<>();
postParamProvider.accept(formParams);
Map<String, String> formMap = new HashMap<>();
for (NameValuePair pair : formParams)
formMap.put(pair.getName(), pair.getValue());
final List<Header> headers = new LinkedList<>();
ClientCredentialsProviderUtils.setClientCredentials(
this.deployment.getAdapterConfig(),
this.deployment.getClientAuthenticator(),
Collections.emptyMap(),
formMap);
new NameValueMapAdapter<>(headers, BasicHeader.class),
new NameValueMapAdapter<>(formParams, BasicNameValuePair.class));
for (Header header : headers)
post.addHeader(header);
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
post.setEntity(form);

View File

@ -0,0 +1,151 @@
package de.acosix.alfresco.keycloak.repo.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.http.NameValuePair;
public class NameValueMapAdapter<T extends NameValuePair> implements Map<String, String> {
private final List<? extends NameValuePair> pairs;
private final Class<T> type;
public NameValueMapAdapter(List<? extends NameValuePair> pairs, Class<T> type) {
this.pairs = pairs;
this.type = type;
}
@Override
public void clear() {
this.pairs.clear();
}
@Override
public boolean containsKey(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return true;
return false;
}
@Override
public boolean containsValue(Object value) {
for (NameValuePair pair : this.pairs)
if (pair.getValue().equals(value))
return true;
return false;
}
@Override
public Set<Entry<String, String>> entrySet() {
Set<Entry<String, String>> set = new HashSet<Entry<String, String>>();
for (NameValuePair pair : this.pairs) {
set.add(new Entry<String, String>() {
@Override
public String getKey() {
return pair.getName();
}
@Override
public String getValue() {
return pair.getValue();
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException();
}
});
}
return set;
}
@Override
public String get(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return pair.getValue();
return null;
}
@Override
public boolean isEmpty() {
return this.pairs.isEmpty();
}
@Override
public Set<String> keySet() {
Set<String> set = new HashSet<>();
for (NameValuePair pair : this.pairs)
set.add(pair.getName());
return set;
}
@Override
public String put(String key, String value) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
i.add(this.newNameValuePair(key, value));
return pair.getValue();
}
}
i.add(this.newNameValuePair(key, value));
return null;
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
for (Entry<? extends String, ? extends String> e : m.entrySet())
this.put(e.getKey(), e.getValue());
}
@Override
public String remove(Object key) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
return pair.getValue();
}
}
return null;
}
@Override
public int size() {
return this.pairs.size();
}
@Override
public Collection<String> values() {
List<String> list = new ArrayList<>(this.pairs.size());
for (NameValuePair pair : this.pairs)
list.add(pair.getValue());
return list;
}
private T newNameValuePair(String key, String value) {
try {
Constructor<T> constructor = this.type.getConstructor(String.class, String.class);
return constructor.newInstance(key, value);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new AlfrescoRuntimeException(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,496 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Copied from Keycloak source: https://raw.githubusercontent.com/keycloak/keycloak/24.0.6/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
*
* The original is not extensible enough to configure the HttpClient with a
* custom HttpRoutePlanner. This copy adds that capability.
*/
package de.acosix.alfresco.keycloak.share.util;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.cookie.Cookie;
import org.apache.http.cookie.CookieSpecProvider;
import org.apache.http.impl.auth.SPNegoSchemeFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CookieSpecRegistries;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.DefaultCookieSpecProvider;
import org.keycloak.adapters.SniSSLSocketFactory;
import org.keycloak.common.util.EnvUtil;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.representations.adapters.config.AdapterHttpClientConfig;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.URI;
import java.security.KeyStore;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Abstraction for creating HttpClients. Allows SSL configuration.
*
* @author <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 boolean disableCookieCache = true;
protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD;
protected SSLContext sslContext;
protected int connectionPoolSize = 100;
protected int maxPooledPerRoute = 0;
protected long connectionTTL = -1;
protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
protected HostnameVerifier verifier = null;
protected long socketTimeout = -1;
protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS;
protected long establishConnectionTimeout = -1;
protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
protected HttpHost proxyHost;
private SPNegoSchemeFactory spNegoSchemeFactory;
private boolean useSpNego;
private HttpRoutePlanner routePlanner;
/**
* 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;
}
public HttpClientBuilder disableCookieCache(boolean disable) {
this.disableCookieCache = 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;
}
public HttpClientBuilder routePlanner(HttpRoutePlanner routePlanner) {
this.routePlanner = routePlanner;
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 HttpClientBuilder spNegoSchemeFactory(SPNegoSchemeFactory spnegoSchemeFactory) {
this.spNegoSchemeFactory = spnegoSchemeFactory;
return this;
}
public HttpClientBuilder useSPNego(boolean useSpnego) {
this.useSpNego = useSpnego;
return this;
}
public HttpClient build() {
X509HostnameVerifier verifier = null;
if (this.verifier != null) verifier = new VerifierWrapper(this.verifier);
else {
switch (policy) {
case ANY:
verifier = new AllowAllHostnameVerifier();
break;
case WILDCARD:
verifier = new BrowserCompatHostnameVerifier();
break;
case STRICT:
verifier = new StrictHostnameVerifier();
break;
}
}
try {
ConnectionSocketFactory sslsf;
SSLContext theContext = sslContext;
if (disableTrustManager) {
theContext = SSLContext.getInstance("SSL");
theContext.init(null, new TrustManager[]{new PassthroughTrustManager()},
new SecureRandom());
verifier = new AllowAllHostnameVerifier();
sslsf = new SniSSLSocketFactory(theContext, verifier);
} else if (theContext != null) {
sslsf = new SniSSLSocketFactory(theContext, verifier);
} else if (clientKeyStore != null || truststore != null) {
sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier);
} else {
final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS);
tlsContext.init(null, null, null);
sslsf = new SniSSLSocketFactory(tlsContext, verifier);
}
RegistryBuilder<ConnectionSocketFactory> sf = RegistryBuilder.create();
sf.register("http", PlainConnectionSocketFactory.getSocketFactory());
sf.register("https", sslsf);
HttpClientConnectionManager cm;
if (connectionPoolSize > 0) {
PoolingHttpClientConnectionManager tcm = new PoolingHttpClientConnectionManager(sf.build(), null, null, null, connectionTTL, connectionTTLUnit);
tcm.setMaxTotal(connectionPoolSize);
if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize;
tcm.setDefaultMaxPerRoute(maxPooledPerRoute);
cm = tcm;
} else {
cm = new BasicHttpClientConnectionManager(sf.build());
}
SocketConfig.Builder socketConfig = SocketConfig.copy(SocketConfig.DEFAULT);
ConnectionConfig.Builder connConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT);
RequestConfig.Builder requestConfig = RequestConfig.copy(RequestConfig.DEFAULT);
if (proxyHost != null) {
requestConfig.setProxy(new HttpHost(proxyHost));
}
if (socketTimeout > -1) {
requestConfig.setSocketTimeout((int) socketTimeoutUnits.toMillis(socketTimeout));
}
if (establishConnectionTimeout > -1) {
requestConfig.setConnectTimeout((int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout));
}
Registry<CookieSpecProvider> cookieSpecs = CookieSpecRegistries.createDefaultBuilder()
.register(CookieSpecs.DEFAULT, new DefaultCookieSpecProvider()).build();
if (useSpNego) {
requestConfig.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.SPNEGO));
}
org.apache.http.impl.client.HttpClientBuilder clientBuilder = org.apache.http.impl.client.HttpClientBuilder.create()
.setDefaultSocketConfig(socketConfig.build())
.setDefaultConnectionConfig(connConfig.build())
.setDefaultRequestConfig(requestConfig.build())
.setDefaultCookieSpecRegistry(cookieSpecs)
.setConnectionManager(cm);
if (spNegoSchemeFactory != null) {
RegistryBuilder<AuthSchemeProvider> authSchemes = RegistryBuilder.create();
authSchemes.register(AuthSchemes.SPNEGO, spNegoSchemeFactory);
clientBuilder.setDefaultAuthSchemeRegistry(authSchemes.build());
}
if (useSpNego) {
Credentials fake = new Credentials() {
@Override
public String getPassword() {
return null;
}
@Override
public Principal getUserPrincipal() {
return null;
}
};
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, fake);
clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
if (disableCookieCache) {
clientBuilder.setDefaultCookieStore(new CookieStore() {
@Override
public void addCookie(Cookie cookie) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public List<Cookie> getCookies() {
return Collections.emptyList();
}
@Override
public boolean clearExpired(Date date) {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void clear() {
//To change body of implemented methods use File | Settings | File Templates.
}
});
}
if (this.routePlanner != null) {
clientBuilder.setRoutePlanner(this.routePlanner);
}
return clientBuilder.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
disableCookieCache(true); // disable cookie cache as we don't want sticky sessions for load balancing
String truststorePath = adapterConfig.getTruststore();
if (truststorePath != null) {
truststorePath = EnvUtil.replace(truststorePath);
String truststorePassword = adapterConfig.getTruststorePassword();
try {
this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore", e);
}
}
String clientKeystore = adapterConfig.getClientKeystore();
if (clientKeystore != null) {
clientKeystore = EnvUtil.replace(clientKeystore);
String clientKeystorePassword = adapterConfig.getClientKeystorePassword();
try {
KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
keyStore(clientCertKeystore, clientKeystorePassword);
} catch (Exception e) {
throw new RuntimeException("Failed to load keystore", e);
}
}
HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD;
if (adapterConfig.isAllowAnyHostname())
policy = HttpClientBuilder.HostnameVerificationPolicy.ANY;
connectionPoolSize(adapterConfig.getConnectionPoolSize());
hostnameVerification(policy);
if (adapterConfig.isDisableTrustManager()) {
disableTrustManager();
} else {
trustStore(truststore);
}
configureProxyForAuthServerIfProvided(adapterConfig);
if (socketTimeout == -1 && adapterConfig.getSocketTimeout() > 0) {
socketTimeout(adapterConfig.getSocketTimeout(), TimeUnit.MILLISECONDS);
}
if (establishConnectionTimeout == -1 && adapterConfig.getConnectionTimeout() > 0) {
establishConnectionTimeout(adapterConfig.getConnectionTimeout(), TimeUnit.MILLISECONDS);
}
if (connectionTTL == -1 && adapterConfig.getConnectionTTL() > 0) {
connectionTTL(adapterConfig.getConnectionTTL(), TimeUnit.MILLISECONDS);
}
return build();
}
/**
* Configures a the proxy to use for auth-server requests if provided.
* <p>
* If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
* given URL as a proxy server, otherwise the proxy configuration is ignored.
* </p>
*
* @param adapterConfig
*/
private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
return;
}
URI uri = URI.create(adapterConfig.getProxyUrl());
this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
}
}

View File

@ -0,0 +1,151 @@
package de.acosix.alfresco.keycloak.share.util;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.http.NameValuePair;
public class NameValueMapAdapter<T extends NameValuePair> implements Map<String, String> {
private final List<? extends NameValuePair> pairs;
private final Class<T> type;
public NameValueMapAdapter(List<? extends NameValuePair> pairs, Class<T> type) {
this.pairs = pairs;
this.type = type;
}
@Override
public void clear() {
this.pairs.clear();
}
@Override
public boolean containsKey(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return true;
return false;
}
@Override
public boolean containsValue(Object value) {
for (NameValuePair pair : this.pairs)
if (pair.getValue().equals(value))
return true;
return false;
}
@Override
public Set<Entry<String, String>> entrySet() {
Set<Entry<String, String>> set = new HashSet<Entry<String, String>>();
for (NameValuePair pair : this.pairs) {
set.add(new Entry<String, String>() {
@Override
public String getKey() {
return pair.getName();
}
@Override
public String getValue() {
return pair.getValue();
}
@Override
public String setValue(String value) {
throw new UnsupportedOperationException();
}
});
}
return set;
}
@Override
public String get(Object key) {
for (NameValuePair pair : this.pairs)
if (pair.getName().equals(key))
return pair.getValue();
return null;
}
@Override
public boolean isEmpty() {
return this.pairs.isEmpty();
}
@Override
public Set<String> keySet() {
Set<String> set = new HashSet<>();
for (NameValuePair pair : this.pairs)
set.add(pair.getName());
return set;
}
@Override
public String put(String key, String value) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
i.add(this.newNameValuePair(key, value));
return pair.getValue();
}
}
i.add(this.newNameValuePair(key, value));
return null;
}
@Override
public void putAll(Map<? extends String, ? extends String> m) {
for (Entry<? extends String, ? extends String> e : m.entrySet())
this.put(e.getKey(), e.getValue());
}
@Override
public String remove(Object key) {
ListIterator<NameValuePair> i = (ListIterator<NameValuePair>) this.pairs.listIterator();
while (i.hasNext()) {
NameValuePair pair = i.next();
if (pair.getName().equals(key)) {
i.remove();
return pair.getValue();
}
}
return null;
}
@Override
public int size() {
return this.pairs.size();
}
@Override
public Collection<String> values() {
List<String> list = new ArrayList<>(this.pairs.size());
for (NameValuePair pair : this.pairs)
list.add(pair.getValue());
return list;
}
private T newNameValuePair(String key, String value) {
try {
Constructor<T> constructor = this.type.getConstructor(String.class, String.class);
return constructor.newInstance(key, value);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new AlfrescoRuntimeException(e.getMessage(), e);
}
}
}

View File

@ -23,13 +23,16 @@ import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -50,8 +53,11 @@ import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.PropertyCheck;
import org.alfresco.web.site.servlet.SSOAuthenticationFilter;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
@ -60,8 +66,11 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.params.ConnRouteParams;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
@ -127,6 +136,8 @@ import de.acosix.alfresco.keycloak.share.config.KeycloakAdapterConfigElement;
import de.acosix.alfresco.keycloak.share.config.KeycloakAuthenticationConfigElement;
import de.acosix.alfresco.keycloak.share.config.KeycloakConfigConstants;
import de.acosix.alfresco.keycloak.share.remote.AccessTokenAwareSlingshotAlfrescoConnector;
import de.acosix.alfresco.keycloak.share.util.HttpClientBuilder;
import de.acosix.alfresco.keycloak.share.util.NameValueMapAdapter;
import de.acosix.alfresco.keycloak.share.util.RefreshableAccessTokenHolder;
/**
@ -517,13 +528,25 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
{
final ExtendedAdapterConfig adapterConfiguration = keycloakAdapterConfig.buildAdapterConfiguration();
this.keycloakDeployment = KeycloakDeploymentBuilder.build(adapterConfiguration);
final String forcedRouteUrl = adapterConfiguration.getForcedRouteUrl();
if (forcedRouteUrl != null && !forcedRouteUrl.isEmpty())
{
final HttpClient client = this.keycloakDeployment.getClient();
this.configureForcedRouteIfNecessary(client, forcedRouteUrl);
this.keycloakDeployment.setClient(client);
// we need to recreate the HttpClient to configure the forced route URL
this.keycloakDeployment.setClient(new Callable<HttpClient>() {
private HttpClient client;
@Override
public HttpClient call() throws Exception {
if (client == null) {
synchronized (this) {
if (client == null) {
client = new HttpClientBuilder()
.routePlanner(createForcedRoutePlanner(adapterConfiguration))
.build(adapterConfiguration);
}
}
}
return client;
}
});
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
}
@ -1728,7 +1751,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.keycloakDeployment.getAuthServerBaseUrl())
.path(ServiceUrlConstants.TOKEN_PATH).build(this.keycloakDeployment.getRealm()));
final List<NameValuePair> formParams = new ArrayList<>();
final List<NameValuePair> formParams = new LinkedList<>();
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, alfrescoResourceName));
@ -1751,16 +1774,16 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
"Either an active security context or access token should be present in the session, or previous validations have caught their non-existence and prevented this operation form being called");
}
Map<String, String> formMap = new HashMap<>();
for (NameValuePair formParam : formParams)
formMap.put(formParam.getName(), formParam.getValue());
final List<Header> headers = new LinkedList<>();
ClientCredentialsProviderUtils.setClientCredentials(
this.keycloakDeployment.getAdapterConfig(),
this.keycloakDeployment.getClientAuthenticator(),
Collections.emptyMap(),
formMap);
new NameValueMapAdapter<>(headers, BasicHeader.class),
new NameValueMapAdapter<>(formParams, BasicNameValuePair.class));
for (Header header : headers)
post.addHeader(header);
final UrlEncodedFormEntity form = new UrlEncodedFormEntity(formParams, "UTF-8");
post.setEntity(form);
@ -1877,4 +1900,47 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
}
params.setParameter(ConnRoutePNames.FORCED_ROUTE, route);
}
protected HttpRoute createRoute(ExtendedAdapterConfig adapterConfig, HttpHost routeHost) throws UnknownHostException, MalformedURLException {
boolean secure = "https".equalsIgnoreCase(routeHost.getSchemeName());
if (adapterConfig.getProxyUrl() != null) {
// useful in parsing the URL for just what is needed for HttpHost
URL proxyUrl = new URL(adapterConfig.getProxyUrl());
HttpHost proxyHost = new HttpHost(proxyUrl.getHost(), proxyUrl.getPort(), proxyUrl.getProtocol());
return new HttpRoute(routeHost, InetAddress.getLocalHost(), proxyHost, secure);
} else {
return new HttpRoute(routeHost, InetAddress.getLocalHost(), secure);
}
}
protected HttpRoute createForcedRoute(ExtendedAdapterConfig adapterConfig) throws UnknownHostException, MalformedURLException {
// useful in parsing the URL for just what is needed for HttpHost
URL forcedRouteUrl = new URL(adapterConfig.getForcedRouteUrl());
HttpHost forcedRouteHost = new HttpHost(forcedRouteUrl.getHost(), forcedRouteUrl.getPort(), forcedRouteUrl.getProtocol());
return this.createRoute(adapterConfig, forcedRouteHost);
}
protected HttpRoutePlanner createForcedRoutePlanner(ExtendedAdapterConfig adapterConfig) throws MalformedURLException {
URL authServerUrl = new URL(adapterConfig.getAuthServerUrl());
final HttpHost authServerHost = new HttpHost(authServerUrl.getHost(), authServerUrl.getPort(), authServerUrl.getProtocol());
return new HttpRoutePlanner() {
@Override
public HttpRoute determineRoute(HttpHost target, HttpRequest request, HttpContext context) throws HttpException {
try {
if (authServerHost.equals(target)) {
LOGGER.trace("Rerouting to forced route");
HttpRoute route = createForcedRoute(adapterConfig);
LOGGER.trace("Rerouting to forced route: {}", route);
return route;
} else {
return createRoute(adapterConfig, target);
}
} catch (IOException ie) {
throw new HttpException(ie.getMessage(), ie);
}
}
};
}
}