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.
+ *
+ * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
+ * given URL as a proxy server, otherwise the proxy configuration is ignored.
+ *
+ *
+ * @param adapterConfig
+ */
+ private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
+
+ if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
+ return;
+ }
+
+ URI uri = URI.create(adapterConfig.getProxyUrl());
+ this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ }
+}
\ No newline at end of file
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/util/NameValueMapAdapter.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/util/NameValueMapAdapter.java
new file mode 100644
index 0000000..fcf494b
--- /dev/null
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/util/NameValueMapAdapter.java
@@ -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 implements Map {
+
+ private final List extends NameValuePair> pairs;
+ private final Class type;
+
+ public NameValueMapAdapter(List extends NameValuePair> pairs, Class 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> entrySet() {
+ Set> set = new HashSet>();
+ for (NameValuePair pair : this.pairs) {
+ set.add(new Entry() {
+ @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 keySet() {
+ Set set = new HashSet<>();
+ for (NameValuePair pair : this.pairs)
+ set.add(pair.getName());
+ return set;
+ }
+
+ @Override
+ public String put(String key, String value) {
+ ListIterator i = (ListIterator) 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 i = (ListIterator) 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 values() {
+ List 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 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);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
index 877d12e..03e796e 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/KeycloakAuthenticationFilter.java
@@ -23,33 +23,41 @@ 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.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
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;
@@ -58,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;
@@ -73,7 +84,6 @@ import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.OIDCAuthenticationError;
import org.keycloak.adapters.OidcKeycloakAccount;
import org.keycloak.adapters.PreAuthActionsHandler;
-import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
import org.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens;
import org.keycloak.adapters.servlet.FilterRequestAuthenticator;
@@ -88,6 +98,7 @@ import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.Time;
import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.protocol.oidc.client.authentication.ClientCredentialsProviderUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
@@ -125,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;
/**
@@ -515,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() {
+ 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);
}
@@ -1726,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 formParams = new ArrayList<>();
+ final List formParams = new LinkedList<>();
formParams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
formParams.add(new BasicNameValuePair(OAuth2Constants.AUDIENCE, alfrescoResourceName));
@@ -1748,9 +1773,17 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
throw new IllegalStateException(
"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");
}
+
+ final List headers = new LinkedList<>();
- ClientCredentialsProviderUtils.setClientCredentials(this.keycloakDeployment, post, formParams);
+ ClientCredentialsProviderUtils.setClientCredentials(
+ this.keycloakDeployment.getAdapterConfig(),
+ this.keycloakDeployment.getClientAuthenticator(),
+ 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);
@@ -1867,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);
+ }
+ }
+ };
+ }
}
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/PopulatingRequestContextInterceptor.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/PopulatingRequestContextInterceptor.java
index 6fd2637..471a3c4 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/PopulatingRequestContextInterceptor.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/PopulatingRequestContextInterceptor.java
@@ -15,7 +15,7 @@
*/
package de.acosix.alfresco.keycloak.share.web;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
import org.springframework.extensions.surf.RequestContext;
import org.springframework.extensions.surf.RequestContextUtil;
@@ -27,7 +27,7 @@ import org.springframework.web.context.request.WebRequest;
/**
* This specialisation of the request context interceptor exists only to ensure that a newly created request context is properly
- * {@link RequestContextUtil#populateRequestContext(org.springframework.extensions.surf.RequestContext, javax.servlet.http.HttpServletRequest)
+ * {@link RequestContextUtil#populateRequestContext(org.springframework.extensions.surf.RequestContext, jakarta.servlet.http.HttpServletRequest)
* populated} as to ensure that somewhat important data, such as the user object, is properly initialised.
*
* @author Axel Faust
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/ResponseHeaderCookieCaptureServletHttpFacade.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/ResponseHeaderCookieCaptureServletHttpFacade.java
index 2c7d637..73fc943 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/ResponseHeaderCookieCaptureServletHttpFacade.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/ResponseHeaderCookieCaptureServletHttpFacade.java
@@ -23,7 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
import org.alfresco.util.Pair;
import org.keycloak.adapters.servlet.ServletHttpFacade;
@@ -39,7 +39,7 @@ import org.keycloak.adapters.spi.HttpFacade;
public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFacade
{
- protected final Map, javax.servlet.http.Cookie> cookies = new HashMap<>();
+ protected final Map, jakarta.servlet.http.Cookie> cookies = new HashMap<>();
protected final Map> headers = new HashMap<>();
@@ -67,7 +67,7 @@ public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFac
/**
* @return the cookies
*/
- public List getCookies()
+ public List getCookies()
{
return new ArrayList<>(this.cookies.values());
}
@@ -137,7 +137,7 @@ public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFac
public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge,
final boolean secure, final boolean httpOnly)
{
- final javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(name, value);
+ final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, value);
cookie.setPath(path);
if (domain != null)
{
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserGroupsLoadFilter.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserGroupsLoadFilter.java
index 200e305..6b0f826 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserGroupsLoadFilter.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserGroupsLoadFilter.java
@@ -18,13 +18,13 @@ package de.acosix.alfresco.keycloak.share.web;
import java.io.IOException;
import java.util.Date;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
import org.alfresco.util.PropertyCheck;
import org.alfresco.web.site.SlingshotUserFactory;
diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java
index 180620d..fe7b150 100644
--- a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java
+++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java
@@ -18,10 +18,10 @@ package de.acosix.alfresco.keycloak.share.web;
import java.util.HashMap;
import java.util.Map;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
import org.alfresco.util.PropertyCheck;
import org.alfresco.web.site.servlet.SlingshotLoginController;