From 6f7910aa930782f90471bdf740c8b0837fe41c57 Mon Sep 17 00:00:00 2001 From: Brian Long Date: Thu, 22 Aug 2024 14:21:39 -0400 Subject: [PATCH] Upgrade to ACS v23.x, Jakarta, jdk17 --- pom.xml | 51 +- repository/pom.xml | 24 +- .../main/config/alfresco-global.properties | 10 +- repository/src/main/config/log4j2.properties | 12 + .../KeycloakAuthenticationFilter.java | 20 +- .../KeycloakRemoteUserMapper.java | 4 +- ...akWebScriptCookieAuthenticationFilter.java | 14 +- ...cloakWebScriptSSOAuthenticationFilter.java | 12 +- ...eHeaderCookieCaptureServletHttpFacade.java | 8 +- .../repo/token/AccessTokenClient.java | 19 +- .../repo/util/NameValueMapAdapter.java | 151 ++++++ share/pom.xml | 19 +- share/src/main/config/log4j2.properties | 15 + ...AccessTokenAwareAlfrescoAuthenticator.java | 2 +- ...sTokenAwareSlingshotAlfrescoConnector.java | 2 +- .../share/util/HttpClientBuilder.java | 496 ++++++++++++++++++ .../share/util/NameValueMapAdapter.java | 151 ++++++ .../web/KeycloakAuthenticationFilter.java | 116 +++- .../PopulatingRequestContextInterceptor.java | 4 +- ...eHeaderCookieCaptureServletHttpFacade.java | 8 +- .../share/web/UserGroupsLoadFilter.java | 14 +- ...ameCorrectingSlingshotLoginController.java | 8 +- 22 files changed, 1052 insertions(+), 108 deletions(-) create mode 100644 repository/src/main/config/log4j2.properties create mode 100644 repository/src/main/java/de/acosix/alfresco/keycloak/repo/util/NameValueMapAdapter.java create mode 100644 share/src/main/config/log4j2.properties create mode 100644 share/src/main/java/de/acosix/alfresco/keycloak/share/util/HttpClientBuilder.java create mode 100644 share/src/main/java/de/acosix/alfresco/keycloak/share/util/NameValueMapAdapter.java diff --git a/pom.xml b/pom.xml index 8ec973d..d7ac3fc 100644 --- a/pom.xml +++ b/pom.xml @@ -20,13 +20,13 @@ de.acosix.alfresco.maven - de.acosix.alfresco.maven.project.parent-6.0.7 - 1.4.1 + de.acosix.alfresco.maven.project.parent-23.1.0 + 1.5.0 de.acosix.alfresco.keycloak de.acosix.alfresco.keycloak.parent - 1.1.0-rc7 + 1.2.0-rc1 pom Acosix Alfresco Keycloak - Parent @@ -61,6 +61,15 @@ twitter.com/ReluctantBird83 + + blong + Brian Long + brian@inteligr8.com + Inteligr8 LLC + + twitter.com/brian_m_long + + @@ -68,18 +77,18 @@ acosix.keycloak acosix-keycloak - 1.8 - 1.8 + 3.13.0 + 3.6.0 - 3.2.4 - - 16.1.0 + 22.0.3 3.15.1.Final 4.5.13 - 4.4.14 + 4.4.16 + + 9.0 - 1.2.5 + 1.4.3 1.1.0.0 @@ -101,6 +110,7 @@ USER alfresco 1.3.0 --> + @@ -132,13 +142,13 @@ org.keycloak - keycloak-servlet-adapter-spi + keycloak-jakarta-servlet-adapter-spi ${keycloak.version} org.keycloak - keycloak-servlet-filter-adapter + keycloak-jakarta-servlet-filter-adapter ${keycloak.version} @@ -150,7 +160,7 @@ org.keycloak - keycloak-admin-client + keycloak-admin-client-jakarta ${keycloak.version} @@ -187,6 +197,13 @@ provided + + org.alfresco.surf + spring-surf + ${surf.version} + provided + + de.acosix.alfresco.utility de.acosix.alfresco.utility.common @@ -298,6 +315,14 @@ + + maven-source-plugin + ${maven.source.version} + + + maven-compiler-plugin + ${maven.compiler.version} + maven-shade-plugin ${maven.shade.version} diff --git a/repository/pom.xml b/repository/pom.xml index 2ceee02..1acf426 100644 --- a/repository/pom.xml +++ b/repository/pom.xml @@ -21,7 +21,7 @@ de.acosix.alfresco.keycloak de.acosix.alfresco.keycloak.parent - 1.1.0-rc7 + 1.2.0-rc1 de.acosix.alfresco.keycloak.repo @@ -44,12 +44,12 @@ - + - javax.servlet - javax.servlet-api - - + jakarta.servlet + jakarta.servlet-api + + org.keycloak keycloak-adapter-core @@ -71,12 +71,17 @@ org.jboss.resteasy * + + + org.apache.httpcomponents + httpclient + org.keycloak - keycloak-servlet-adapter-spi + keycloak-jakarta-servlet-adapter-spi @@ -105,7 +110,7 @@ org.keycloak - keycloak-servlet-filter-adapter + keycloak-jakarta-servlet-filter-adapter @@ -233,7 +238,8 @@ shade - true + + false true false diff --git a/repository/src/main/config/alfresco-global.properties b/repository/src/main/config/alfresco-global.properties index ad180f0..bc21536 100644 --- a/repository/src/main/config/alfresco-global.properties +++ b/repository/src/main/config/alfresco-global.properties @@ -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 diff --git a/repository/src/main/config/log4j2.properties b/repository/src/main/config/log4j2.properties new file mode 100644 index 0000000..1501180 --- /dev/null +++ b/repository/src/main/config/log4j2.properties @@ -0,0 +1,12 @@ + +logger.acosix-alfresco-keycloak.name=${project.artifactId} +logger.acosix-alfresco-keycloak.level=INFO + +logger.acosix-alfresco-keycloak-deps.name=${project.artifactId}.deps +logger.acosix-alfresco-keycloak-deps.level=ERROR + +logger.acosix-alfresco-keycloak-deps-keycloak.name=${project.artifactId}.deps.keycloak +logger.acosix-alfresco-keycloak-deps-keycloak.level=ERROR + +logger.acosix-alfresco-keycloak-deps-jboss.name=${project.artifactId}.deps.jboss +logger.acosix-alfresco-keycloak-deps-jboss.level=ERROR diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java index 03d1a18..53ee61a 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java @@ -21,16 +21,16 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; -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.repo.SessionUser; import org.alfresco.repo.cache.SimpleCache; diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java index a4f525d..24c75cf 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java @@ -17,8 +17,8 @@ package de.acosix.alfresco.keycloak.repo.authentication; import java.util.List; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; import org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.security.authentication.AuthenticationException; diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptCookieAuthenticationFilter.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptCookieAuthenticationFilter.java index ce859dd..28dd529 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptCookieAuthenticationFilter.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptCookieAuthenticationFilter.java @@ -17,13 +17,13 @@ package de.acosix.alfresco.keycloak.repo.authentication; import java.io.IOException; -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.repo.SessionUser; import org.alfresco.repo.web.scripts.bean.LoginPost; diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptSSOAuthenticationFilter.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptSSOAuthenticationFilter.java index a4ace4b..8fa48ee 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptSSOAuthenticationFilter.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakWebScriptSSOAuthenticationFilter.java @@ -17,12 +17,12 @@ package de.acosix.alfresco.keycloak.repo.authentication; import java.io.IOException; -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 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 org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter; diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/ResponseHeaderCookieCaptureServletHttpFacade.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/ResponseHeaderCookieCaptureServletHttpFacade.java index 8b1828a..31d955a 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/ResponseHeaderCookieCaptureServletHttpFacade.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/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<>(); @@ -71,7 +71,7 @@ public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFac /** * @return the cookies */ - public List getCookies() + public List getCookies() { return new ArrayList<>(this.cookies.values()); } @@ -157,7 +157,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/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java index 4c06cc0..6ae9f92 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/token/AccessTokenClient.java @@ -4,37 +4,40 @@ 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.LinkedList; import java.util.List; 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; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.ServerRequest.HttpFailure; -import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.adapters.rotation.AdapterTokenVerifier; import org.keycloak.adapters.rotation.AdapterTokenVerifier.VerifiedTokens; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.acosix.alfresco.keycloak.repo.util.NameValueMapAdapter; import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder; /** @@ -282,12 +285,20 @@ public class AccessTokenClient final HttpPost post = new HttpPost(KeycloakUriBuilder.fromUri(this.deployment.getAuthServerBaseUrl()) .path(ServiceUrlConstants.TOKEN_PATH).build(this.deployment.getRealm())); - final List formParams = new ArrayList<>(); + final List formParams = new LinkedList<>(); postParamProvider.accept(formParams); + + final List
headers = new LinkedList<>(); - ClientCredentialsProviderUtils.setClientCredentials(this.deployment, post, formParams); + ClientCredentialsProviderUtils.setClientCredentials( + this.deployment.getAdapterConfig(), + this.deployment.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); diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/util/NameValueMapAdapter.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/util/NameValueMapAdapter.java new file mode 100644 index 0000000..cb13219 --- /dev/null +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/util/NameValueMapAdapter.java @@ -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 implements Map { + + private final List pairs; + private final Class type; + + public NameValueMapAdapter(List 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 m) { + for (Entry 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/pom.xml b/share/pom.xml index 3e5ed86..1938a03 100644 --- a/share/pom.xml +++ b/share/pom.xml @@ -21,7 +21,7 @@ de.acosix.alfresco.keycloak de.acosix.alfresco.keycloak.parent - 1.1.0-rc7 + 1.2.0-rc1 de.acosix.alfresco.keycloak.share @@ -45,8 +45,8 @@ org.bouncycastle - bcpkix-jdk15on - 1.68 + bcpkix-jdk18on + 1.77 @@ -61,8 +61,8 @@ - javax.servlet - javax.servlet-api + jakarta.servlet + jakarta.servlet-api @@ -83,7 +83,7 @@ org.keycloak - keycloak-servlet-adapter-spi + keycloak-jakarta-servlet-adapter-spi @@ -104,7 +104,7 @@ org.keycloak - keycloak-servlet-filter-adapter + keycloak-jakarta-servlet-filter-adapter @@ -132,7 +132,7 @@ org.bouncycastle - bcpkix-jdk15on + bcpkix-jdk18on @@ -284,7 +284,8 @@ shade - true + + false true false diff --git a/share/src/main/config/log4j2.properties b/share/src/main/config/log4j2.properties new file mode 100644 index 0000000..f01f616 --- /dev/null +++ b/share/src/main/config/log4j2.properties @@ -0,0 +1,15 @@ + +logger.acosix-alfresco-keycloak.name=${project.artifactId} +logger.acosix-alfresco-keycloak.level=INFO + +logger.acosix-alfresco-keycloak-deps.name=${project.artifactId}.deps +logger.acosix-alfresco-keycloak-deps.level=ERROR + +logger.acosix-alfresco-keycloak-deps-keycloak.name=${project.artifactId}.deps.keycloak +logger.acosix-alfresco-keycloak-deps-keycloak.level=ERROR + +logger.acosix-alfresco-keycloak-deps-jackson.name=${project.artifactId}.deps.jackson +logger.acosix-alfresco-keycloak-deps-jackson.level=ERROR + +logger.acosix-alfresco-keycloak-deps-jboss.name=${project.artifactId}.deps.jboss +logger.acosix-alfresco-keycloak-deps-jboss.level=ERROR diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareAlfrescoAuthenticator.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareAlfrescoAuthenticator.java index 3dec055..fb4359c 100644 --- a/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareAlfrescoAuthenticator.java +++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareAlfrescoAuthenticator.java @@ -15,7 +15,7 @@ */ package de.acosix.alfresco.keycloak.share.remote; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; import org.keycloak.adapters.OidcKeycloakAccount; import org.keycloak.adapters.spi.KeycloakAccount; diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareSlingshotAlfrescoConnector.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareSlingshotAlfrescoConnector.java index 86f6b0f..ee43cf8 100644 --- a/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareSlingshotAlfrescoConnector.java +++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/remote/AccessTokenAwareSlingshotAlfrescoConnector.java @@ -17,7 +17,7 @@ package de.acosix.alfresco.keycloak.share.remote; import java.util.Collections; -import javax.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession; import org.alfresco.web.site.servlet.SlingshotAlfrescoConnector; import org.keycloak.KeycloakSecurityContext; diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/util/HttpClientBuilder.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/util/HttpClientBuilder.java new file mode 100644 index 0000000..cfb9697 --- /dev/null +++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/util/HttpClientBuilder.java @@ -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 Bill Burke +* @version $Revision: 1 $ +*/ +public class HttpClientBuilder { + + public static enum HostnameVerificationPolicy { + /** + * Hostname verification is not done on the server's certificate + */ + ANY, + /** + * Allows wildcards in subdomain names i.e. *.foo.com + */ + WILDCARD, + /** + * CN must match hostname connecting to + */ + STRICT + } + + + /** + * @author Bill Burke + * @version $Revision: 1 $ + */ + private static class PassthroughTrustManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, + String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + protected KeyStore truststore; + protected KeyStore clientKeyStore; + protected String clientPrivateKeyPassword; + protected boolean disableTrustManager; + protected boolean disableCookieCache = true; + protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD; + protected SSLContext sslContext; + protected int connectionPoolSize = 100; + protected int maxPooledPerRoute = 0; + protected long connectionTTL = -1; + protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS; + protected HostnameVerifier verifier = null; + protected long socketTimeout = -1; + protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS; + protected long establishConnectionTimeout = -1; + protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS; + protected HttpHost proxyHost; + 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. NOTE this is a security + * hole, so only set this option if you cannot or do not want to verify the identity of the + * host you are communicating with. + */ + public HttpClientBuilder disableTrustManager() { + this.disableTrustManager = true; + return this; + } + + public HttpClientBuilder disableCookieCache(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 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 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 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 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 pairs; + private final Class type; + + public NameValueMapAdapter(List 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 m) { + for (Entry 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;