mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-26 21:44:41 +00:00
General working Keycloak filter state
This commit is contained in:
parent
d857dbc9a3
commit
ad7f404846
15
pom.xml
15
pom.xml
@ -78,6 +78,7 @@
|
||||
<apache.httpcore.version>4.4.3</apache.httpcore.version>
|
||||
|
||||
<acosix.utility.version>1.0.7.0</acosix.utility.version>
|
||||
<ootbee.support-tools.version>1.1.0.0</ootbee.support-tools.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -176,6 +177,20 @@
|
||||
<classifier>installable</classifier>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.orderofthebee.support-tools</groupId>
|
||||
<artifactId>support-tools-repo</artifactId>
|
||||
<version>${ootbee.support-tools.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.orderofthebee.support-tools</groupId>
|
||||
<artifactId>support-tools-share</artifactId>
|
||||
<version>${ootbee.support-tools.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
@ -3,4 +3,6 @@ module.title=${project.name}
|
||||
module.description=${project.description}
|
||||
module.version=${noSnapshotVersion}
|
||||
|
||||
module.repo.version.min=5
|
||||
module.repo.version.min=5
|
||||
|
||||
module.depends.acosix-utility-core=1.0.3.1-*
|
@ -28,22 +28,135 @@
|
||||
<name>Acosix Alfresco Keycloak - Repository Module</name>
|
||||
|
||||
<properties>
|
||||
<!-- Alfresco 6.x bundles an old version of adapter libraries -->
|
||||
<!-- adapt dependencies to match -->
|
||||
<keycloak.version>4.6.0.Final</keycloak.version>
|
||||
<docker.tests.keycloakPort>8380</docker.tests.keycloakPort>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-repository</artifactId>
|
||||
<artifactId>alfresco-remote-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- not included / packaged by Alfresco 6.x - only SPI -->
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-servlet-filter-adapter</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.acosix.alfresco.utility</groupId>
|
||||
<artifactId>de.acosix.alfresco.utility.core.repo</artifactId>
|
||||
<classifier>installable</classifier>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.orderofthebee.support-tools</groupId>
|
||||
<artifactId>support-tools-repo</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
|
||||
<!-- some image customisations -->
|
||||
<!-- Maven + docker-maven-plugin result in somewhat weird inheritance handling -->
|
||||
<!-- (relying on positional order of images for overrides) -->
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<images>
|
||||
<image>
|
||||
<!-- no change to postgres image (first image in parent POM) -->
|
||||
</image>
|
||||
<image>
|
||||
<!-- customise repository image (second image in parent POM) -->
|
||||
<run>
|
||||
<env>
|
||||
<DOCKER_HOST_NAME>${docker.tests.host.name}</DOCKER_HOST_NAME>
|
||||
</env>
|
||||
<!-- add log directory mount to just the contentstore -->
|
||||
<!-- (cannot be done in parent POM due to hard requirement on specific project structure -->
|
||||
<!-- for tests to easily check contentstore files, we also mount alf_data locally, not in a volume -->
|
||||
<volumes>
|
||||
<bind>
|
||||
<volume>${moduleId}-repository-test-contentstore:/usr/local/tomcat/alf_data</volume>
|
||||
<volume>${project.build.directory}/docker/repository-logs:/usr/local/tomcat/logs</volume>
|
||||
</bind>
|
||||
</volumes>
|
||||
<dependsOn>
|
||||
<container>postgres</container>
|
||||
<container>keycloak</container>
|
||||
</dependsOn>
|
||||
</run>
|
||||
</image>
|
||||
<image>
|
||||
<!-- no change to Share image (we don't use it) -->
|
||||
</image>
|
||||
<image>
|
||||
<!-- no change to Search image (we don't use it) -->
|
||||
</image>
|
||||
<image>
|
||||
<name>jboss/keycloak</name>
|
||||
<alias>keycloak</alias>
|
||||
<run>
|
||||
<hostname>keycloak</hostname>
|
||||
<env>
|
||||
<KEYCLOAK_USER>admin</KEYCLOAK_USER>
|
||||
<KEYCLOAK_PASSWORD>admin</KEYCLOAK_PASSWORD>
|
||||
<KEYCLOAK_IMPORT>/tmp/test-realm.json</KEYCLOAK_IMPORT>
|
||||
<DB_VENDOR>h2</DB_VENDOR>
|
||||
</env>
|
||||
<ports>
|
||||
<port>${docker.tests.keycloakPort}:8080</port>
|
||||
</ports>
|
||||
<network>
|
||||
<mode>custom</mode>
|
||||
<name>${moduleId}-test</name>
|
||||
<alias>keycloak</alias>
|
||||
</network>
|
||||
<volumes>
|
||||
<bind>
|
||||
<volume>${project.build.directory}/docker/test-realm.json:/tmp/test-realm.json</volume>
|
||||
</bind>
|
||||
</volumes>
|
||||
</run>
|
||||
</image>
|
||||
</images>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@ -0,0 +1,51 @@
|
||||
cache.${moduleId}.ssoToSessionCache.maxItems=10000
|
||||
cache.${moduleId}.ssoToSessionCache.timeToLiveSeconds=0
|
||||
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.readBackupData=false
|
||||
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
||||
cache.${moduleId}.ssoToSessionCache.clearable=false
|
||||
# replicate, not distribute
|
||||
cache.${moduleId}.ssoToSessionCache.ignite.cache.type=replicated
|
||||
|
||||
cache.${moduleId}.sessionToSsoCache.maxItems=10000
|
||||
cache.${moduleId}.sessionToSsoCache.timeToLiveSeconds=0
|
||||
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.readBackupData=false
|
||||
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
||||
cache.${moduleId}.sessionToSsoCache.clearable=false
|
||||
# replicate, not distribute
|
||||
cache.${moduleId}.sessionToSsoCache.ignite.cache.type=replicated
|
||||
|
||||
cache.${moduleId}.principalToSessionCache.maxItems=10000
|
||||
cache.${moduleId}.principalToSessionCache.timeToLiveSeconds=0
|
||||
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.readBackupData=false
|
||||
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
||||
cache.${moduleId}.principalToSessionCache.clearable=false
|
||||
# replicate, not distribute
|
||||
cache.${moduleId}.principalToSessionCache.ignite.cache.type=replicated
|
||||
|
||||
cache.${moduleId}.sessionToPrincipalCache.maxItems=10000
|
||||
cache.${moduleId}.sessionToPrincipalCache.timeToLiveSeconds=0
|
||||
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.readBackupData=false
|
||||
# explicitly not clearable - should be cleared via Keycloak back-channel action
|
||||
cache.${moduleId}.sessionToPrincipalCache.clearable=false
|
||||
# replicate, not distribute
|
||||
cache.${moduleId}.sessionToPrincipalCache.ignite.cache.type=replicated
|
@ -19,6 +19,32 @@
|
||||
http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
|
||||
|
||||
<!-- this is required for this module to work at all -->
|
||||
<bean id="${moduleId}-enhanceAuthenticationChildApplicationContextManager"
|
||||
class="de.acosix.alfresco.utility.common.spring.ImplementationClassReplacingBeanDefinitionRegistryPostProcessor">
|
||||
<property name="enabled" value="true" />
|
||||
<property name="propertiesSource" ref="global-properties" />
|
||||
|
||||
<property name="targetBeanName" value="Authentication" />
|
||||
<property name="originalClassName" value="org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager" />
|
||||
<property name="replacementClassName"
|
||||
value="de.acosix.alfresco.utility.repo.subsystems.SubsystemChildApplicationContextManager" />
|
||||
</bean>
|
||||
|
||||
<bean name="${moduleId}-sessionIdMapper-ssoToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
|
||||
</bean>
|
||||
|
||||
<bean name="${moduleId}-sessionIdMapper-sessionToSsoCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||
<constructor-arg value="cache.${moduleId}.sessionToSsoCache" />
|
||||
</bean>
|
||||
|
||||
<bean name="${moduleId}-sessionIdMapper-principalToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||
<constructor-arg value="cache.${moduleId}.principalToSessionCache" />
|
||||
</bean>
|
||||
|
||||
<bean name="${moduleId}-sessionIdMapper-sessionToPrincipalCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||
<constructor-arg value="cache.${moduleId}.sessionToPrincipalCache" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
@ -0,0 +1,137 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!--
|
||||
Copyright 2019 Acosix GmbH
|
||||
|
||||
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.
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
|
||||
|
||||
<bean id="subsystem-properties" class="de.acosix.alfresco.utility.repo.subsystems.SubsystemEffectivePropertiesFactoryBean">
|
||||
<property name="subsystemChildApplicationContextManager" ref="Authentication" />
|
||||
</bean>
|
||||
|
||||
<bean id="keycloakAdapterConfig" class="${project.artifactId}.spring.KeycloakAdapterConfigBeanFactory">
|
||||
<property name="propertiesSource" ref="subsystem-properties" />
|
||||
<property name="configPropertyPrefix" value="keycloak.adapter" />
|
||||
</bean>
|
||||
|
||||
<bean id="keycloakDeployment" class="${project.artifactId}.spring.KeycloakDeploymentBeanFactory">
|
||||
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
||||
<property name="connectionTimeout" value="${keycloak.authentication.connectionTimeout}" />
|
||||
<property name="socketTimeout" value="${keycloak.authentication.socketTimeout}" />
|
||||
</bean>
|
||||
|
||||
<bean id="sessionIdMapper" class="${project.artifactId}.authentication.SimpleCacheBackedSessionIdMapper">
|
||||
<property name="ssoToSession" ref="${moduleId}-sessionIdMapper-ssoToSessionCache" />
|
||||
<property name="sessionToSso" ref="${moduleId}-sessionIdMapper-sessionToSsoCache" />
|
||||
<property name="principalToSession" ref="${moduleId}-sessionIdMapper-principalToSessionCache" />
|
||||
<property name="sessionToPrincipal" ref="${moduleId}-sessionIdMapper-sessionToPrincipalCache" />
|
||||
</bean>
|
||||
|
||||
<bean id="authenticationComponent" class="${project.artifactId}.authentication.KeycloakAuthenticationComponent"
|
||||
parent="authenticationComponentBase">
|
||||
<property name="nodeService" ref="nodeService" />
|
||||
<property name="personService" ref="personService" />
|
||||
<property name="transactionService" ref="transactionService" />
|
||||
<property name="active" value="${keycloak.authentication.enabled}" />
|
||||
<property name="defaultAdministratorUserNameList" value="${keycloak.authentication.defaultAdministratorUserNames}" />
|
||||
<property name="allowUserNamePasswordLogin" value="${keycloak.authentication.allowUserNamePasswordLogin}" />
|
||||
<property name="allowGuestLogin" value="${keycloak.authentication.allowGuestLogin}" />
|
||||
<property name="adapterConfig" ref="keycloakAdapterConfig" />
|
||||
<property name="connectionTimeout" value="${keycloak.authentication.connectionTimeout}" />
|
||||
<property name="socketTimeout" value="${keycloak.authentication.socketTimeout}" />
|
||||
</bean>
|
||||
|
||||
<!-- Wrapped version to be used within subsystem -->
|
||||
<bean id="AuthenticationComponent" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
|
||||
<property name="proxyInterfaces">
|
||||
<list>
|
||||
<value>org.alfresco.repo.security.authentication.AuthenticationComponent</value>
|
||||
</list>
|
||||
</property>
|
||||
<property name="transactionManager" ref="transactionManager" />
|
||||
<property name="target" ref="authenticationComponent" />
|
||||
<property name="transactionAttributes">
|
||||
<props>
|
||||
<prop key="*">${server.transaction.mode.default}</prop>
|
||||
</props>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!-- Authentication service for chaining -->
|
||||
<bean id="localAuthenticationService" class="org.alfresco.repo.security.authentication.AuthenticationServiceImpl">
|
||||
<property name="ticketComponent" ref="ticketComponent" />
|
||||
<property name="authenticationComponent" ref="authenticationComponent" />
|
||||
<property name="sysAdminParams" ref="sysAdminParams" />
|
||||
<property name="protectedUsersCache" ref="protectedUsersCache" />
|
||||
<property name="protectionEnabled" value="${authentication.protection.enabled}" />
|
||||
<property name="protectionLimit" value="${authentication.protection.limit}" />
|
||||
<property name="protectionPeriodSeconds" value="${authentication.protection.periodSeconds}" />
|
||||
<property name="personService" ref="personService" />
|
||||
</bean>
|
||||
|
||||
<bean id="ftpAuthenticator" class="org.alfresco.filesys.auth.ftp.AlfrescoFtpAuthenticator" parent="ftpAuthenticatorBase">
|
||||
<property name="active" value="${keycloak.authentication.authenticateFTP}" />
|
||||
</bean>
|
||||
|
||||
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
||||
<property name="nodeService" ref="nodeService" />
|
||||
<property name="authorityService" ref="authorityService" />
|
||||
<property name="tenantService" ref="tenantService" />
|
||||
<property name="namespaceService" ref="namespaceService" />
|
||||
<property name="compositePasswordEncoder" ref="compositePasswordEncoder" />
|
||||
<property name="policyComponent" ref="policyComponent" />
|
||||
<property name="authenticationCache" ref="authenticationCache" />
|
||||
<property name="singletonCache" ref="immutableSingletonCache" />
|
||||
<property name="transactionService" ref="transactionService" />
|
||||
</bean>
|
||||
|
||||
<bean id="remoteUserMapper" class="${project.artifactId}.authentication.KeycloakRemoteUserMapper">
|
||||
<property name="active" value="${keycloak.authentication.enabled}" />
|
||||
<property name="validationFailureSilent" value="${keycloak.authentication.silentValidationFailure}" />
|
||||
<property name="keycloakDeployment" ref="keycloakDeployment" />
|
||||
<property name="personService" ref="PersonService" />
|
||||
</bean>
|
||||
|
||||
<bean id="webscriptAuthenticationFilter" class="org.alfresco.web.app.servlet.WebScriptSSOAuthenticationFilter">
|
||||
<property name="active" value="${keycloak.authentication.enabled}" />
|
||||
<property name="authenticationService" ref="AuthenticationService" />
|
||||
<property name="authenticationComponent" ref="AuthenticationComponent" />
|
||||
<property name="personService" ref="personService" />
|
||||
<property name="nodeService" ref="NodeService" />
|
||||
<property name="transactionService" ref="TransactionService" />
|
||||
<property name="container" ref="webscripts.container" />
|
||||
</bean>
|
||||
|
||||
<bean id="globalAuthenticationFilter" class="${project.artifactId}.authentication.KeycloakAuthenticationFilter">
|
||||
<property name="active" value="${keycloak.authentication.sso.enabled}" />
|
||||
<property name="allowTicketLogon" value="${keycloak.authentication.allowTicketLogons}" />
|
||||
<property name="allowLocalBasicLogon" value="${keycloak.authentication.allowLocalBasicLogon}" />
|
||||
<property name="loginPageUrl" value="${keycloak.authentication.loginPageUrl}" />
|
||||
<property name="bodyBufferLimit" value="${keycloak.authentication.bodyBufferLimit}" />
|
||||
<property name="sslRedirectPort" value="${keycloak.authentication.sslRedirectPort}" />
|
||||
<property name="keycloakDeployment" ref="keycloakDeployment" />
|
||||
<property name="sessionIdMapper" ref="sessionIdMapper" />
|
||||
|
||||
<property name="authenticationService" ref="AuthenticationService" />
|
||||
<property name="authenticationComponent" ref="AuthenticationComponent" />
|
||||
<property name="authenticationListener" ref="globalAuthenticationListener" />
|
||||
<property name="personService" ref="personService" />
|
||||
<property name="nodeService" ref="NodeService" />
|
||||
<property name="transactionService" ref="TransactionService" />
|
||||
<property name="remoteUserMapper" ref="RemoteUserMapper" />
|
||||
</bean>
|
||||
</beans>
|
@ -0,0 +1,25 @@
|
||||
keycloak.authentication.enabled=true
|
||||
keycloak.authentication.sso.enabled=true
|
||||
keycloak.authentication.defaultAdministratorUserNames=
|
||||
keycloak.authentication.allowTicketLogons=true
|
||||
keycloak.authentication.allowLocalBasicLogon=true
|
||||
keycloak.authentication.allowUserNamePasswordLogin=true
|
||||
keycloak.authentication.allowGuestLogin=true
|
||||
keycloak.authentication.authenticateFTP=true
|
||||
keycloak.authentication.silentValidationFailure=true
|
||||
|
||||
keycloak.authentication.connectionTimeout=-1
|
||||
keycloak.authentication.socketTimeout=-1
|
||||
keycloak.authentication.sslRedirectPort=8443
|
||||
keycloak.authentication.bodyBufferLimit=10485760
|
||||
|
||||
keycloak.adapter.auth-server-url=http://localhost:8180/auth
|
||||
keycloak.adapter.direct-auth-server-url=${keycloak.adapter.auth-server-url}
|
||||
keycloak.adapter.realm=alfresco
|
||||
keycloak.adapter.resource=alfresco
|
||||
keycloak.adapter.ssl-required=none
|
||||
keycloak.adapter.public-client=false
|
||||
keycloak.adapter.credentials.provider=secret
|
||||
keycloak.adapter.credentials.secret=
|
||||
|
||||
# TODO default settings (identical to AdapterConfig defaults) to better align with default Alfresco subsystem property handling
|
@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.authorization.client.AuthzClient;
|
||||
import org.keycloak.authorization.client.Configuration;
|
||||
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean, InitializingBean
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationComponent.class);
|
||||
|
||||
protected boolean active;
|
||||
|
||||
protected boolean allowUserNamePasswordLogin;
|
||||
|
||||
protected boolean allowGuestLogin;
|
||||
|
||||
protected AdapterConfig adapterConfig;
|
||||
|
||||
protected int connectionTimeout;
|
||||
|
||||
protected int socketTimeout;
|
||||
|
||||
protected Configuration config;
|
||||
|
||||
protected AuthzClient authzClient;
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "adapterConfig", this.adapterConfig);
|
||||
|
||||
if (this.allowUserNamePasswordLogin)
|
||||
{
|
||||
Map<String, Object> credentials = this.adapterConfig.getCredentials();
|
||||
if (credentials != null)
|
||||
{
|
||||
credentials = new HashMap<>(credentials);
|
||||
}
|
||||
|
||||
if (credentials == null || ((!credentials.containsKey("provider") || "secret".equals(credentials.get("provider")))
|
||||
&& !credentials.containsKey("secret")))
|
||||
{
|
||||
if (credentials == null)
|
||||
{
|
||||
credentials = new HashMap<>();
|
||||
}
|
||||
credentials.put("secret", "");
|
||||
}
|
||||
|
||||
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
|
||||
if (this.connectionTimeout > 0)
|
||||
{
|
||||
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(this.connectionTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
if (this.socketTimeout > 0)
|
||||
{
|
||||
httpClientBuilder = httpClientBuilder.socketTimeout(this.socketTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
this.config = new Configuration(this.adapterConfig.getAuthServerUrl(), this.adapterConfig.getRealm(),
|
||||
this.adapterConfig.getResource(), credentials, httpClientBuilder.build(this.adapterConfig));
|
||||
try
|
||||
{
|
||||
this.authzClient = AuthzClient.create(this.config);
|
||||
}
|
||||
catch (final RuntimeException e)
|
||||
{
|
||||
if (LOGGER.isDebugEnabled())
|
||||
{
|
||||
LOGGER.debug("Failed to pre-instantiate Keycloak authz client", e);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.warn("Failed to pre-instantiate Keycloak authz client: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param active
|
||||
* the active to set
|
||||
*/
|
||||
public void setActive(final boolean active)
|
||||
{
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allowUserNamePasswordLogin
|
||||
* the allowUserNamePasswordLogin to set
|
||||
*/
|
||||
public void setAllowUserNamePasswordLogin(final boolean allowUserNamePasswordLogin)
|
||||
{
|
||||
this.allowUserNamePasswordLogin = allowUserNamePasswordLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allowGuestLogin
|
||||
* the allowGuestLogin to set
|
||||
*/
|
||||
public void setAllowGuestLogin(final boolean allowGuestLogin)
|
||||
{
|
||||
this.allowGuestLogin = allowGuestLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param adapterConfig
|
||||
* the adapterConfig to set
|
||||
*/
|
||||
public void setAdapterConfig(final AdapterConfig adapterConfig)
|
||||
{
|
||||
this.adapterConfig = adapterConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param connectionTimeout
|
||||
* the connectionTimeout to set
|
||||
*/
|
||||
public void setConnectionTimeout(final int connectionTimeout)
|
||||
{
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param socketTimeout
|
||||
* the socketTimeout to set
|
||||
*/
|
||||
public void setSocketTimeout(final int socketTimeout)
|
||||
{
|
||||
this.socketTimeout = socketTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isActive()
|
||||
{
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void authenticateImpl(final String userName, final char[] password) throws AuthenticationException
|
||||
{
|
||||
if (!this.allowUserNamePasswordLogin)
|
||||
{
|
||||
throw new AuthenticationException("Simple login via user name + password is not allowed");
|
||||
}
|
||||
|
||||
if (this.authzClient == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.authzClient = AuthzClient.create(this.config);
|
||||
}
|
||||
catch (final RuntimeException e)
|
||||
{
|
||||
LOGGER.warn("Failed to pre-instantiate Keycloak authz client", e);
|
||||
throw new AuthenticationException("Keycloak authentication cannot be performed", e);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.authzClient.obtainAccessToken(userName, new String(password));
|
||||
this.setCurrentUser(userName);
|
||||
}
|
||||
catch (final HttpResponseException e)
|
||||
{
|
||||
LOGGER.debug("Failed to authenticate user against Keycloak. Status: {} Reason: {}", e.getStatusCode(), e.getReasonPhrase());
|
||||
throw new AuthenticationException("Failed to authenticate user against Keycloak.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected boolean implementationAllowsGuestLogin()
|
||||
{
|
||||
return this.allowGuestLogin;
|
||||
}
|
||||
}
|
@ -0,0 +1,778 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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 org.alfresco.repo.SessionUser;
|
||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||
import org.alfresco.repo.security.authentication.Authorization;
|
||||
import org.alfresco.repo.web.auth.BasicAuthCredentials;
|
||||
import org.alfresco.repo.web.auth.TicketCredentials;
|
||||
import org.alfresco.repo.web.auth.UnknownCredentials;
|
||||
import org.alfresco.repo.web.filter.beans.DependencyInjectedFilter;
|
||||
import org.alfresco.repo.webdav.auth.AuthenticationDriver;
|
||||
import org.alfresco.repo.webdav.auth.BaseAuthenticationFilter;
|
||||
import org.alfresco.repo.webdav.auth.BaseSSOAuthenticationFilter;
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||
import org.keycloak.adapters.AuthenticatedActionsHandler;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OidcKeycloakAccount;
|
||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||
import org.keycloak.adapters.servlet.FilterRequestAuthenticator;
|
||||
import org.keycloak.adapters.servlet.OIDCFilterSessionStore;
|
||||
import org.keycloak.adapters.servlet.OIDCServletHttpFacade;
|
||||
import org.keycloak.adapters.spi.AuthOutcome;
|
||||
import org.keycloak.adapters.spi.AuthenticationError;
|
||||
import org.keycloak.adapters.spi.KeycloakAccount;
|
||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||
import org.keycloak.adapters.spi.UserSessionManagement;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* This class provides a Keycloak-based authentication filter which can be used in the role of both global and WebDAV authentication filter.
|
||||
*
|
||||
* This class does not use the Alfresco default base {@link BaseSSOAuthenticationFilter SSO} filter class as a base class for inheritance
|
||||
* since these classes are extremely NTLM / Kerberos centric and would require extremely weird hacks / workarounds to use its constraints to
|
||||
* implement a Keycloak-based authentication.
|
||||
*
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
||||
implements InitializingBean, ActivateableBean, DependencyInjectedFilter
|
||||
{
|
||||
|
||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class);
|
||||
|
||||
private static final String KEYCLOAK_ACTION_URL_PATTERN = "^(?:/wcs(?:ervice)?)?/keycloak/k_[^/]+$";
|
||||
|
||||
private static final int DEFAULT_BODY_BUFFER_LIMIT = 32 * 1024;// 32 KiB
|
||||
|
||||
protected boolean active;
|
||||
|
||||
protected boolean allowTicketLogon;
|
||||
|
||||
protected boolean allowLocalBasicLogon;
|
||||
|
||||
protected String loginPageUrl;
|
||||
|
||||
protected int bodyBufferLimit = DEFAULT_BODY_BUFFER_LIMIT;
|
||||
|
||||
// use 8443 as default SSL redirect based on Tomcat default server.xml configuration
|
||||
// can't rely on SysAdminParams#getAlfrescoPort either because that may be proxied / non-SSL
|
||||
protected int sslRedirectPort = 8443;
|
||||
|
||||
protected KeycloakDeployment keycloakDeployment;
|
||||
|
||||
protected SessionIdMapper sessionIdMapper;
|
||||
|
||||
protected AdapterDeploymentContext deploymentContext;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "keycloakDeployment", this.keycloakDeployment);
|
||||
PropertyCheck.mandatory(this, "sessionIdMapper", this.sessionIdMapper);
|
||||
|
||||
// parent class does not check, so we do
|
||||
PropertyCheck.mandatory(this, "authenticationService", this.authenticationService);
|
||||
PropertyCheck.mandatory(this, "authenticationComponent", this.authenticationComponent);
|
||||
PropertyCheck.mandatory(this, "authenticationListener", this.authenticationListener);
|
||||
PropertyCheck.mandatory(this, "personService", this.personService);
|
||||
PropertyCheck.mandatory(this, "nodeService", this.nodeService);
|
||||
PropertyCheck.mandatory(this, "transactionService", this.transactionService);
|
||||
|
||||
this.deploymentContext = new AdapterDeploymentContext(this.keycloakDeployment);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isActive()
|
||||
{
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param active
|
||||
* the active to set
|
||||
*/
|
||||
public void setActive(final boolean active)
|
||||
{
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allowTicketLogon
|
||||
* the allowTicketLogon to set
|
||||
*/
|
||||
public void setAllowTicketLogon(final boolean allowTicketLogon)
|
||||
{
|
||||
this.allowTicketLogon = allowTicketLogon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allowLocalBasicLogon
|
||||
* the allowLocalBasicLogon to set
|
||||
*/
|
||||
public void setAllowLocalBasicLogon(final boolean allowLocalBasicLogon)
|
||||
{
|
||||
this.allowLocalBasicLogon = allowLocalBasicLogon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loginPageUrl
|
||||
* the loginPageUrl to set
|
||||
*/
|
||||
public void setLoginPageUrl(final String loginPageUrl)
|
||||
{
|
||||
this.loginPageUrl = loginPageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bodyBufferLimit
|
||||
* the bodyBufferLimit to set
|
||||
*/
|
||||
public void setBodyBufferLimit(final int bodyBufferLimit)
|
||||
{
|
||||
this.bodyBufferLimit = bodyBufferLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sslRedirectPort
|
||||
* the sslRedirectPort to set
|
||||
*/
|
||||
public void setSslRedirectPort(final int sslRedirectPort)
|
||||
{
|
||||
this.sslRedirectPort = sslRedirectPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keycloakDeployment
|
||||
* the keycloakDeployment to set
|
||||
*/
|
||||
public void setKeycloakDeployment(final KeycloakDeployment keycloakDeployment)
|
||||
{
|
||||
this.keycloakDeployment = keycloakDeployment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sessionIdMapper
|
||||
* the sessionIdMapper to set
|
||||
*/
|
||||
public void setSessionIdMapper(final SessionIdMapper sessionIdMapper)
|
||||
{
|
||||
this.sessionIdMapper = sessionIdMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(final ServletContext context, final ServletRequest request, final ServletResponse response,
|
||||
final FilterChain chain) throws IOException, ServletException
|
||||
{
|
||||
final HttpServletRequest req = (HttpServletRequest) request;
|
||||
final HttpServletResponse res = (HttpServletResponse) response;
|
||||
|
||||
final boolean skip = this.checkForSkipCondition(context, req, res);
|
||||
|
||||
if (skip)
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.checkAndProcessLocalBasicAuthentication(req))
|
||||
{
|
||||
this.processKeycloakAuthenticationAndActions(context, req, res, chain);
|
||||
}
|
||||
else
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and processes any HTTP Basic authentication against the local Alfresco authentication services if allowed.
|
||||
*
|
||||
* @param req
|
||||
* the servlet request
|
||||
*
|
||||
* @throws IOException
|
||||
* if any error occurs during processing of HTTP Basic authentication
|
||||
* @throws ServletException
|
||||
* if any error occurs during processing of HTTP Basic authentication
|
||||
*
|
||||
* @return {@code true} if an existing HTTP Basic authentication header was successfully processed against the local Alfresco
|
||||
* authentication services, {@code false} otherwise
|
||||
*/
|
||||
protected boolean checkAndProcessLocalBasicAuthentication(final HttpServletRequest req) throws IOException, ServletException
|
||||
{
|
||||
boolean basicAuthSucessfull = false;
|
||||
final String authHeader = req.getHeader(HEADER_AUTHORIZATION);
|
||||
if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
|
||||
{
|
||||
final String basicAuth = new String(Base64.decodeBase64(authHeader.substring(6).getBytes(StandardCharsets.UTF_8)),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
String userName;
|
||||
String password = "";
|
||||
|
||||
final int pos = basicAuth.indexOf(":");
|
||||
if (pos != -1)
|
||||
{
|
||||
userName = basicAuth.substring(0, pos);
|
||||
password = basicAuth.substring(pos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
userName = basicAuth;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (userName.equalsIgnoreCase(Authorization.TICKET_USERID))
|
||||
{
|
||||
if (this.allowTicketLogon)
|
||||
{
|
||||
LOGGER.trace("Performing HTTP Basic ticket validation");
|
||||
this.authenticationService.validate(password);
|
||||
|
||||
this.createUserEnvironment(req.getSession(), this.authenticationService.getCurrentUserName(),
|
||||
this.authenticationService.getCurrentTicket(), false);
|
||||
|
||||
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using an authentication ticket",
|
||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||
|
||||
this.authenticationListener.userAuthenticated(new TicketCredentials(password));
|
||||
|
||||
basicAuthSucessfull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.debug("Ticket in HTTP Basic authentication header detected but ticket logon is disabled");
|
||||
}
|
||||
}
|
||||
else if (this.allowLocalBasicLogon)
|
||||
{
|
||||
LOGGER.trace("Performing HTTP Basic user authentication against local Alfresco services");
|
||||
|
||||
this.authenticationService.authenticate(userName, password.toCharArray());
|
||||
|
||||
this.createUserEnvironment(req.getSession(), this.authenticationService.getCurrentUserName(),
|
||||
this.authenticationService.getCurrentTicket(), false);
|
||||
|
||||
LOGGER.debug("Authenticated user {} via HTTP Basic authentication using locally stored credentials",
|
||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||
|
||||
this.authenticationListener.userAuthenticated(new BasicAuthCredentials(userName, password));
|
||||
|
||||
basicAuthSucessfull = true;
|
||||
}
|
||||
}
|
||||
catch (final AuthenticationException e)
|
||||
{
|
||||
LOGGER.debug("HTTP Basic authentication against local Alfresco services failed", e);
|
||||
|
||||
if (userName.equalsIgnoreCase(Authorization.TICKET_USERID))
|
||||
{
|
||||
this.authenticationListener.authenticationFailed(new TicketCredentials(password), e);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.authenticationListener.authenticationFailed(new BasicAuthCredentials(userName, password), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return basicAuthSucessfull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes Keycloak authentication and potential action operations. If a Keycloak action has been processed, the request processing
|
||||
* will be terminated. Otherwise processing may continue with the filter chain (if still applicable).
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the servlet request
|
||||
* @param res
|
||||
* the servlet response
|
||||
* @param chain
|
||||
* the filter chain
|
||||
* @throws IOException
|
||||
* if any error occurs during Keycloak authentication or processing of the filter chain
|
||||
* @throws ServletException
|
||||
* if any error occurs during Keycloak authentication or processing of the filter chain
|
||||
*/
|
||||
protected void processKeycloakAuthenticationAndActions(final ServletContext context, final HttpServletRequest req,
|
||||
final HttpServletResponse res, final FilterChain chain) throws IOException, ServletException
|
||||
{
|
||||
LOGGER.trace("Processing Keycloak authentication and actions on request to {}", req.getRequestURL());
|
||||
|
||||
final OIDCServletHttpFacade facade = new OIDCServletHttpFacade(req, res);
|
||||
|
||||
final String servletPath = req.getServletPath();
|
||||
final String pathInfo = req.getPathInfo();
|
||||
final String servletRequestUri = servletPath + (pathInfo != null ? pathInfo : "");
|
||||
if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
|
||||
{
|
||||
LOGGER.trace("Applying Keycloak pre-auth actions handler");
|
||||
final PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement()
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void logoutAll()
|
||||
{
|
||||
KeycloakAuthenticationFilter.this.sessionIdMapper.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void logoutHttpSessions(final List<String> ids)
|
||||
{
|
||||
ids.forEach(KeycloakAuthenticationFilter.this.sessionIdMapper::removeSession);
|
||||
}
|
||||
}, this.deploymentContext, facade);
|
||||
|
||||
if (preActions.handleRequest())
|
||||
{
|
||||
LOGGER.debug("Keycloak pre-auth actions processed the request - stopping filter chain execution");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, facade,
|
||||
this.bodyBufferLimit > 0 ? this.bodyBufferLimit : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment, this.sessionIdMapper);
|
||||
final FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(this.keycloakDeployment, tokenStore, facade, req,
|
||||
this.sslRedirectPort);
|
||||
final AuthOutcome authOutcome = authenticator.authenticate();
|
||||
|
||||
if (authOutcome == AuthOutcome.AUTHENTICATED)
|
||||
{
|
||||
this.onKeycloakAuthenticationSuccess(context, req, res, chain, facade, tokenStore);
|
||||
}
|
||||
else if (authOutcome == AuthOutcome.NOT_ATTEMPTED)
|
||||
{
|
||||
LOGGER.trace("No authentication took place - sending authentication challenge");
|
||||
authenticator.getChallenge().challenge(facade);
|
||||
}
|
||||
else if (authOutcome == AuthOutcome.FAILED)
|
||||
{
|
||||
this.onKeycloakAuthenticationFailure(context, req, res);
|
||||
|
||||
LOGGER.trace("Sending authentication challenge from failure");
|
||||
authenticator.getChallenge().challenge(facade);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a sucessfull authentication via Keycloak.
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the servlet request
|
||||
* @param res
|
||||
* the servlet response
|
||||
* @param chain
|
||||
* the filter chain
|
||||
* @param facade
|
||||
* the Keycloak HTTP facade
|
||||
* @param tokenStore
|
||||
* the Keycloak token store
|
||||
* @throws IOException
|
||||
* if any error occurs during Keycloak authentication or processing of the filter chain
|
||||
* @throws ServletException
|
||||
* if any error occurs during Keycloak authentication or processing of the filter chain
|
||||
*/
|
||||
protected void onKeycloakAuthenticationSuccess(final ServletContext context, final HttpServletRequest req,
|
||||
final HttpServletResponse res, final FilterChain chain, final OIDCServletHttpFacade facade,
|
||||
final OIDCFilterSessionStore tokenStore) throws IOException, ServletException
|
||||
{
|
||||
final HttpSession session = req.getSession();
|
||||
final Object keycloakAccount = session != null ? session.getAttribute(KeycloakAccount.class.getName()) : null;
|
||||
if (keycloakAccount instanceof OidcKeycloakAccount)
|
||||
{
|
||||
final KeycloakSecurityContext keycloakSecurityContext = ((OidcKeycloakAccount) keycloakAccount).getKeycloakSecurityContext();
|
||||
final AccessToken accessToken = keycloakSecurityContext.getToken();
|
||||
final String userId = accessToken.getPreferredUsername();
|
||||
|
||||
LOGGER.debug("User {} successfully authenticated via Keycloak", AuthenticationUtil.maskUsername(userId));
|
||||
|
||||
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
|
||||
// need different attribute name than default for integration with web scripts framework
|
||||
// default attribute name seems to be no longer used
|
||||
session.setAttribute(AuthenticationDriver.AUTHENTICATION_USER, sessionUser);
|
||||
|
||||
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
|
||||
}
|
||||
|
||||
if (facade.isEnded())
|
||||
{
|
||||
LOGGER.debug("Keycloak authenticator processed the request - stopping filter chain execution");
|
||||
return;
|
||||
}
|
||||
|
||||
final String servletPath = req.getServletPath();
|
||||
final String pathInfo = req.getPathInfo();
|
||||
final String servletRequestUri = servletPath + (pathInfo != null ? pathInfo : "");
|
||||
|
||||
if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
|
||||
{
|
||||
LOGGER.trace("Applying Keycloak authenticated actions handler");
|
||||
final AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(this.keycloakDeployment, facade);
|
||||
if (actions.handledRequest())
|
||||
{
|
||||
LOGGER.debug("Keycloak authenticated actions processed the request - stopping filter chain execution");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.trace("Continueing with filter chain processing");
|
||||
final HttpServletRequestWrapper requestWrapper = tokenStore.buildWrapper();
|
||||
chain.doFilter(requestWrapper, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a failed authentication via Keycloak.
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the servlet request
|
||||
* @param res
|
||||
* the servlet response
|
||||
*
|
||||
* @throws IOException
|
||||
* if any error occurs during processing of the filter chain
|
||||
* @throws ServletException
|
||||
* if any error occurs during processing of the filter chain
|
||||
*/
|
||||
protected void onKeycloakAuthenticationFailure(final ServletContext context, final HttpServletRequest req,
|
||||
final HttpServletResponse res) throws IOException, ServletException
|
||||
{
|
||||
final Object authenticationError = req.getAttribute(AuthenticationError.class.getName());
|
||||
if (authenticationError != null)
|
||||
{
|
||||
LOGGER.warn("Keycloak authentication failed due to {}", authenticationError);
|
||||
}
|
||||
LOGGER.trace("Resetting session and state cookie before continueing with filter chain");
|
||||
|
||||
req.getSession().invalidate();
|
||||
|
||||
this.resetStateCookies(context, req, res);
|
||||
|
||||
this.authenticationListener.authenticationFailed(new UnknownCredentials());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if processing of the filter must be skipped for the specified request.
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the servlet request to check for potential conditions to skip
|
||||
* @param res
|
||||
* the servlet response on which potential updates of cookies / response headers need to be set
|
||||
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
|
||||
* operation} must be skipped, {@code false} otherwise
|
||||
*
|
||||
* @throws IOException
|
||||
* if any error occurs during inspection of the request
|
||||
* @throws ServletException
|
||||
* if any error occurs during inspection of the request
|
||||
*/
|
||||
protected boolean checkForSkipCondition(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
boolean skip = false;
|
||||
|
||||
final String authHeader = req.getHeader(HEADER_AUTHORIZATION);
|
||||
|
||||
final String servletPath = req.getServletPath();
|
||||
final String pathInfo = req.getPathInfo();
|
||||
final String servletRequestUri = servletPath + (pathInfo != null ? pathInfo : "");
|
||||
|
||||
final SessionUser sessionUser = this.getSessionUser(context, req, res, true);
|
||||
HttpSession session = req.getSession();
|
||||
|
||||
// check for back-channel logout (sessionIdMapper should now of all authenticated sessions)
|
||||
if (this.active && sessionUser != null && session.getAttribute(KeycloakAccount.class.getName()) != null
|
||||
&& !this.sessionIdMapper.hasSession(session.getId()))
|
||||
{
|
||||
LOGGER.debug("Session {} for Keycloak-authenticated user {} was invalidated by back-channel logout", session.getId(),
|
||||
AuthenticationUtil.maskUsername(sessionUser.getUserName()));
|
||||
this.invalidateSession(req);
|
||||
session = req.getSession(false);
|
||||
}
|
||||
|
||||
if (!this.active)
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as filter is not active");
|
||||
skip = true;
|
||||
}
|
||||
else if (req.getAttribute(NO_AUTH_REQUIRED) != null)
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as filter higher up in chain determined authentication as not required");
|
||||
}
|
||||
else if (servletRequestUri.matches(KEYCLOAK_ACTION_URL_PATTERN))
|
||||
{
|
||||
LOGGER.trace("Explicitly not skipping doFilter as Keycloak action URL is being called");
|
||||
}
|
||||
else if (req.getParameter("state") != null && req.getParameter("code") != null && this.hasStateCookie(req))
|
||||
{
|
||||
LOGGER.trace(
|
||||
"Explicitly not skipping doFilter as state and code query parameters of OAuth2 redirect as well as state cookie are present");
|
||||
}
|
||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
|
||||
{
|
||||
LOGGER.trace("Explicitly not skipping doFilter as Bearer authorization header is present");
|
||||
}
|
||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
|
||||
{
|
||||
LOGGER.trace("Explicitly not skipping doFilter as Basic authorization header is present");
|
||||
}
|
||||
else if (authHeader != null)
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as non-OIDC / non-Basic authorization header is present");
|
||||
skip = true;
|
||||
}
|
||||
else if (this.allowTicketLogon && this.checkForTicketParameter(context, req, res))
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as user was authenticated by ticket URL parameter");
|
||||
}
|
||||
else if (sessionUser != null)
|
||||
{
|
||||
final KeycloakAccount keycloakAccount = (KeycloakAccount) session.getAttribute(KeycloakAccount.class.getName());
|
||||
if (keycloakAccount != null)
|
||||
{
|
||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, sessionUser.getUserName(), keycloakAccount);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as non-Keycloak-authenticated session is already established");
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
// TODO Check for login page URL (rarely configured since Repository by default has no login page since 5.0)
|
||||
|
||||
return skip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an existing Keycloak authentication, verifying the state of the underlying access token and potentially refreshing it if
|
||||
* necessary or configured.
|
||||
*
|
||||
* @param req
|
||||
* the HTTP servlet request
|
||||
* @param res
|
||||
* the HTTP servlet response
|
||||
* @param userId
|
||||
* the ID of the authenticated user
|
||||
* @param keycloakAccount
|
||||
* the Keycloak account object
|
||||
* @return {@code true} if processing of the {@link #doFilter(ServletContext, ServletRequest, ServletResponse, FilterChain) filter
|
||||
* operation} can be skipped as the account represents a valid and still active authentication, {@code false} otherwise
|
||||
*/
|
||||
protected boolean validateAndRefreshKeycloakAuthentication(final HttpServletRequest req, final HttpServletResponse res,
|
||||
final String userId, final KeycloakAccount keycloakAccount)
|
||||
{
|
||||
final OIDCServletHttpFacade facade = new OIDCServletHttpFacade(req, res);
|
||||
|
||||
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, facade,
|
||||
this.bodyBufferLimit > 0 ? this.bodyBufferLimit : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment, null);
|
||||
|
||||
final String oldSessionId = req.getSession().getId();
|
||||
|
||||
tokenStore.checkCurrentToken();
|
||||
|
||||
final HttpSession currentSession = req.getSession(false);
|
||||
|
||||
boolean skip = false;
|
||||
if (currentSession != null)
|
||||
{
|
||||
LOGGER.trace("Skipping doFilter as Keycloak-authentication session is still valid");
|
||||
skip = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.sessionIdMapper.removeSession(oldSessionId);
|
||||
LOGGER.debug("Keycloak-authenticated session for user {} was invalidated after token expiration",
|
||||
AuthenticationUtil.maskUsername(userId));
|
||||
}
|
||||
return skip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request has specified a ticket parameter to bypass the standard authentication.
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the request
|
||||
* @param resp
|
||||
* the response
|
||||
*
|
||||
* @throws IOException
|
||||
* if any error occurs during ticket processing
|
||||
* @throws ServletException
|
||||
* if any error occurs during ticket processing
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
// copied + adapted from BaseSSOAuthenticationFilter
|
||||
protected boolean checkForTicketParameter(final ServletContext context, final HttpServletRequest req, final HttpServletResponse resp)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
boolean ticketValid = false;
|
||||
final String ticket = req.getParameter(ARG_TICKET);
|
||||
|
||||
if (ticket != null && ticket.length() != 0)
|
||||
{
|
||||
LOGGER.trace("Logon via ticket from {} ({}:{}) ticket={}", req.getRemoteHost(), req.getRemoteAddr(), req.getRemotePort(),
|
||||
ticket);
|
||||
|
||||
try
|
||||
{
|
||||
final SessionUser user = this.getSessionUser(context, req, resp, true);
|
||||
|
||||
if (user != null && !ticket.equals(user.getTicket()))
|
||||
{
|
||||
LOGGER.debug("Invalidating current session as URL-provided authentication ticket does not match");
|
||||
this.invalidateSession(req);
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
this.authenticationService.validate(ticket);
|
||||
|
||||
this.createUserEnvironment(req.getSession(), this.authenticationService.getCurrentUserName(),
|
||||
this.authenticationService.getCurrentTicket(), true);
|
||||
|
||||
LOGGER.debug("Authenticated user {} via URL-provided authentication ticket",
|
||||
AuthenticationUtil.maskUsername(this.authenticationService.getCurrentUserName()));
|
||||
|
||||
this.authenticationListener.userAuthenticated(new TicketCredentials(ticket));
|
||||
}
|
||||
|
||||
ticketValid = true;
|
||||
}
|
||||
catch (final AuthenticationException authErr)
|
||||
{
|
||||
LOGGER.debug("Failed to authenticate user ticket: {}", authErr.getMessage(), authErr);
|
||||
|
||||
this.authenticationListener.authenticationFailed(new TicketCredentials(ticket), authErr);
|
||||
}
|
||||
}
|
||||
|
||||
return ticketValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the HTTP request has set the Keycloak state cookie.
|
||||
*
|
||||
* @param req
|
||||
* the HTTP request to check
|
||||
* @return {@code true} if the state cookie is set, {@code false} otherwise
|
||||
*/
|
||||
protected boolean hasStateCookie(final HttpServletRequest req)
|
||||
{
|
||||
final String stateCookieName = this.keycloakDeployment.getStateCookieName();
|
||||
final Cookie[] cookies = req.getCookies();
|
||||
final boolean hasStateCookie = cookies != null
|
||||
? Arrays.asList(cookies).stream().map(Cookie::getName).filter(stateCookieName::equals).findAny().isPresent()
|
||||
: false;
|
||||
return hasStateCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets any Keycloak-related state cookies present in the current request.
|
||||
*
|
||||
* @param context
|
||||
* the servlet context
|
||||
* @param req
|
||||
* the servlet request
|
||||
* @param res
|
||||
* the servlet response
|
||||
*/
|
||||
protected void resetStateCookies(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res)
|
||||
{
|
||||
final Cookie[] cookies = req.getCookies();
|
||||
if (cookies != null)
|
||||
{
|
||||
final String stateCookieName = this.keycloakDeployment.getStateCookieName();
|
||||
Arrays.asList(cookies).stream().filter(cookie -> stateCookieName.equals(cookie.getName())).findAny().ifPresent(cookie -> {
|
||||
final Cookie resetCookie = new Cookie(cookie.getName(), "");
|
||||
resetCookie.setPath(context.getContextPath());
|
||||
resetCookie.setMaxAge(0);
|
||||
resetCookie.setHttpOnly(false);
|
||||
resetCookie.setSecure(false);
|
||||
res.addCookie(resetCookie);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected Log getLogger()
|
||||
{
|
||||
return LogFactory.getLog(KeycloakAuthenticationFilter.class);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import org.alfresco.repo.web.auth.WebCredentials;
|
||||
import org.alfresco.util.ParameterCheck;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakCredentials implements WebCredentials
|
||||
{
|
||||
|
||||
private static final long serialVersionUID = -4815212606223856908L;
|
||||
|
||||
private final AccessToken accessToken;
|
||||
|
||||
public KeycloakCredentials(final AccessToken accessToken)
|
||||
{
|
||||
ParameterCheck.mandatory("accessToken", accessToken);
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.accessToken.getId().hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(final Object obj)
|
||||
{
|
||||
if (this == obj)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (this.getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final KeycloakCredentials other = (KeycloakCredentials) obj;
|
||||
final boolean equal = this.accessToken.getId().equals(other.accessToken.getId());
|
||||
return equal;
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.spi.AuthOutcome;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakRemoteUserMapper implements RemoteUserMapper, ActivateableBean, InitializingBean
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakRemoteUserMapper.class);
|
||||
|
||||
protected boolean active;
|
||||
|
||||
protected boolean validationFailureSilent;
|
||||
|
||||
protected KeycloakDeployment keycloakDeployment;
|
||||
|
||||
protected PersonService personService;
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "keycloakDeployment", this.keycloakDeployment);
|
||||
PropertyCheck.mandatory(this, "personService", this.personService);
|
||||
|
||||
this.keycloakDeployment.setBearerOnly(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param active
|
||||
* the active to set
|
||||
*/
|
||||
public void setActive(final boolean active)
|
||||
{
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param validationFailureSilent
|
||||
* the validationFailureSilent to set
|
||||
*/
|
||||
public void setValidationFailureSilent(final boolean validationFailureSilent)
|
||||
{
|
||||
this.validationFailureSilent = validationFailureSilent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keycloakDeployment
|
||||
* the keycloakDeployment to set
|
||||
*/
|
||||
public void setKeycloakDeployment(final KeycloakDeployment keycloakDeployment)
|
||||
{
|
||||
this.keycloakDeployment = keycloakDeployment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param personService
|
||||
* the personService to set
|
||||
*/
|
||||
public void setPersonService(final PersonService personService)
|
||||
{
|
||||
this.personService = personService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isActive()
|
||||
{
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getRemoteUser(final HttpServletRequest request)
|
||||
{
|
||||
String remoteUser = null;
|
||||
if (this.active)
|
||||
{
|
||||
final ResponseHeaderCookieCaptureServletHttpFacade httpFacade = new ResponseHeaderCookieCaptureServletHttpFacade(request);
|
||||
final BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(this.keycloakDeployment);
|
||||
final AuthOutcome authOutcome = authenticator.authenticate(httpFacade);
|
||||
|
||||
if (authOutcome == AuthOutcome.AUTHENTICATED)
|
||||
{
|
||||
final String preferredUsername = authenticator.getToken().getPreferredUsername();
|
||||
final String normalisedUserName = AuthenticationUtil
|
||||
.runAsSystem(() -> this.personService.getUserIdentifier(preferredUsername));
|
||||
|
||||
LOGGER.debug("Authenticated user {} via bearer token, normalised as {}", preferredUsername, normalisedUserName);
|
||||
|
||||
remoteUser = normalisedUserName;
|
||||
}
|
||||
else if (authOutcome == AuthOutcome.FAILED)
|
||||
{
|
||||
authenticator.getChallenge().challenge(httpFacade);
|
||||
final List<String> authenticateHeader = httpFacade.getHeaders().get("WWW-Authenticate");
|
||||
String errorDescription = null;
|
||||
if (authenticateHeader != null && !authenticateHeader.isEmpty())
|
||||
{
|
||||
final String headerValue = authenticateHeader.get(0);
|
||||
final int idx = headerValue.indexOf(", error_description=\"");
|
||||
if (idx != -1)
|
||||
{
|
||||
final int startIdx = idx + ", error_description=\"".length();
|
||||
errorDescription = headerValue.substring(startIdx, headerValue.indexOf('"', startIdx));
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Bearer token authentication failed due to: {}", errorDescription);
|
||||
|
||||
if (!this.validationFailureSilent)
|
||||
{
|
||||
throw new AuthenticationException("Token validation failed: " + errorDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remoteUser;
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.alfresco.util.Pair;
|
||||
import org.keycloak.adapters.servlet.ServletHttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
|
||||
/**
|
||||
* This {@link HttpFacade} wraps servlet requests and responses in such a way that any response headers / cookies being set by Keycloak
|
||||
* authenticators are captured, and otherwise no output is written to the servlet response. This is required for some scenarios in which a
|
||||
* redirect action should be included in the login form.
|
||||
*
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFacade
|
||||
{
|
||||
|
||||
protected final Map<Pair<String, String>, javax.servlet.http.Cookie> cookies = new HashMap<>();
|
||||
|
||||
protected final Map<String, List<String>> headers = new HashMap<>();
|
||||
|
||||
protected int status = -1;
|
||||
|
||||
protected String message;
|
||||
|
||||
/**
|
||||
* Creates a new instance of this class for the provided servlet request.
|
||||
*
|
||||
* @param request
|
||||
* the servlet request to facade
|
||||
*/
|
||||
public ResponseHeaderCookieCaptureServletHttpFacade(final HttpServletRequest request)
|
||||
{
|
||||
super(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Response getResponse()
|
||||
{
|
||||
return new ResponseCaptureFacade();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the cookies
|
||||
*/
|
||||
public List<javax.servlet.http.Cookie> getCookies()
|
||||
{
|
||||
return new ArrayList<>(this.cookies.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the headers
|
||||
*/
|
||||
public Map<String, List<String>> getHeaders()
|
||||
{
|
||||
final Map<String, List<String>> headers = new HashMap<>();
|
||||
this.headers.forEach((headerName, values) -> headers.put(headerName, new ArrayList<>(values)));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the status
|
||||
*/
|
||||
public int getStatus()
|
||||
{
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the message
|
||||
*/
|
||||
public String getMessage()
|
||||
{
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Axel Faust
|
||||
*/
|
||||
private class ResponseCaptureFacade implements Response
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setStatus(final int status)
|
||||
{
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void addHeader(final String name, final String value)
|
||||
{
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setHeader(final String name, final String value)
|
||||
{
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.headers.put(name, new ArrayList<>(Collections.singleton(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void resetCookie(final String name, final String path)
|
||||
{
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.cookies.remove(new Pair<>(name, path));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
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);
|
||||
cookie.setPath(path);
|
||||
if (domain != null)
|
||||
{
|
||||
cookie.setDomain(domain);
|
||||
}
|
||||
cookie.setMaxAge(maxAge);
|
||||
cookie.setSecure(secure);
|
||||
cookie.setHttpOnly(httpOnly);
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.cookies.put(new Pair<>(name, path), cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OutputStream getOutputStream()
|
||||
{
|
||||
return new ByteArrayOutputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void sendError(final int code)
|
||||
{
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.status = code;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void sendError(final int code, final String message)
|
||||
{
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.status = code;
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void end()
|
||||
{
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.authentication;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.alfresco.repo.cache.SimpleCache;
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class SimpleCacheBackedSessionIdMapper implements SessionIdMapper, InitializingBean
|
||||
{
|
||||
|
||||
protected SimpleCache<String, String> ssoToSession;
|
||||
|
||||
protected SimpleCache<String, String> sessionToSso;
|
||||
|
||||
protected SimpleCache<String, Set<String>> principalToSession;
|
||||
|
||||
protected SimpleCache<String, String> sessionToPrincipal;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "ssoToSession", this.ssoToSession);
|
||||
PropertyCheck.mandatory(this, "sessionToSso", this.sessionToSso);
|
||||
PropertyCheck.mandatory(this, "principalToSession", this.principalToSession);
|
||||
PropertyCheck.mandatory(this, "sessionToPrincipal", this.sessionToPrincipal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ssoToSession
|
||||
* the ssoToSession to set
|
||||
*/
|
||||
public void setSsoToSession(final SimpleCache<String, String> ssoToSession)
|
||||
{
|
||||
this.ssoToSession = ssoToSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sessionToSso
|
||||
* the sessionToSso to set
|
||||
*/
|
||||
public void setSessionToSso(final SimpleCache<String, String> sessionToSso)
|
||||
{
|
||||
this.sessionToSso = sessionToSso;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param principalToSession
|
||||
* the principalToSession to set
|
||||
*/
|
||||
public void setPrincipalToSession(final SimpleCache<String, Set<String>> principalToSession)
|
||||
{
|
||||
this.principalToSession = principalToSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sessionToPrincipal
|
||||
* the sessionToPrincipal to set
|
||||
*/
|
||||
public void setSessionToPrincipal(final SimpleCache<String, String> sessionToPrincipal)
|
||||
{
|
||||
this.sessionToPrincipal = sessionToPrincipal;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean hasSession(final String id)
|
||||
{
|
||||
final boolean hasSession = this.sessionToSso.contains(id) || this.sessionToPrincipal.contains(id);
|
||||
return hasSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void clear()
|
||||
{
|
||||
this.ssoToSession.clear();
|
||||
this.sessionToSso.clear();
|
||||
this.principalToSession.clear();
|
||||
this.sessionToPrincipal.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Set<String> getUserSessions(final String principal)
|
||||
{
|
||||
Set<String> userSessions = Collections.emptySet();
|
||||
final Set<String> lookup = this.principalToSession.get(principal);
|
||||
if (lookup != null)
|
||||
{
|
||||
userSessions = new HashSet<>(lookup);
|
||||
}
|
||||
return userSessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getSessionFromSSO(final String sso)
|
||||
{
|
||||
return this.ssoToSession.get(sso);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void map(final String sso, final String principal, final String session)
|
||||
{
|
||||
if (sso != null)
|
||||
{
|
||||
this.ssoToSession.put(sso, session);
|
||||
this.sessionToSso.put(session, sso);
|
||||
}
|
||||
|
||||
if (principal != null)
|
||||
{
|
||||
Set<String> userSessions = this.principalToSession.get(principal);
|
||||
if (userSessions == null)
|
||||
{
|
||||
userSessions = new HashSet<>();
|
||||
this.principalToSession.put(principal, userSessions);
|
||||
|
||||
}
|
||||
userSessions.add(session);
|
||||
this.sessionToPrincipal.put(session, principal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void removeSession(final String session)
|
||||
{
|
||||
final String sso = this.sessionToSso.get(session);
|
||||
this.sessionToSso.remove(session);
|
||||
if (sso != null)
|
||||
{
|
||||
this.ssoToSession.remove(sso);
|
||||
}
|
||||
final String principal = this.sessionToPrincipal.get(session);
|
||||
this.sessionToPrincipal.remove(session);
|
||||
if (principal != null)
|
||||
{
|
||||
final Set<String> sessions = this.principalToSession.get(principal);
|
||||
sessions.remove(session);
|
||||
if (sessions.isEmpty())
|
||||
{
|
||||
this.principalToSession.remove(principal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.spring;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakAdapterConfigBeanFactory implements FactoryBean<AdapterConfig>, InitializingBean
|
||||
{
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdapterConfigBeanFactory.class);
|
||||
|
||||
private static final Map<String, Method> SETTER_BY_CONFIG_NAME;
|
||||
|
||||
private static final Map<String, Class<?>> VALUE_TYPE_BY_CONFIG_NAME;
|
||||
|
||||
private static final List<String> CONFIG_NAMES;
|
||||
|
||||
static
|
||||
{
|
||||
final Map<String, Method> setterByConfigName = new HashMap<>();
|
||||
final Map<String, Class<?>> valueTypeByConfigName = new HashMap<>();
|
||||
final List<String> configNames = new ArrayList<>();
|
||||
|
||||
final Set<Class<?>> supportedValueTypes = new HashSet<>(Arrays.asList(String.class, Map.class));
|
||||
final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new HashMap<>();
|
||||
final Class<?>[] wrapperTypes = { Integer.class, Long.class, Boolean.class, Short.class, Byte.class, Character.class, Float.class,
|
||||
Double.class };
|
||||
final Class<?>[] primitiveTypes = { int.class, long.class, boolean.class, short.class, byte.class, char.class, float.class,
|
||||
double.class };
|
||||
for (int i = 0; i < primitiveTypes.length; i++)
|
||||
{
|
||||
supportedValueTypes.add(primitiveTypes[i]);
|
||||
supportedValueTypes.add(wrapperTypes[i]);
|
||||
primitiveWrapperTypeMap.put(primitiveTypes[i], wrapperTypes[i]);
|
||||
}
|
||||
|
||||
Class<?> cls = AdapterConfig.class;
|
||||
while (cls != null && !Object.class.equals(cls))
|
||||
{
|
||||
final Field[] fields = cls.getDeclaredFields();
|
||||
for (final Field field : fields)
|
||||
{
|
||||
final JsonProperty annotation = field.getAnnotation(JsonProperty.class);
|
||||
if (annotation != null)
|
||||
{
|
||||
final String configName = annotation.value();
|
||||
|
||||
final String fieldName = field.getName();
|
||||
final StringBuilder setterNameBuilder = new StringBuilder(3 + fieldName.length());
|
||||
setterNameBuilder.append("set");
|
||||
setterNameBuilder.append(fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH));
|
||||
setterNameBuilder.append(fieldName.substring(1));
|
||||
final String setterName = setterNameBuilder.toString();
|
||||
|
||||
Class<?> valueType = field.getType();
|
||||
try
|
||||
{
|
||||
final Method setter = cls.getDeclaredMethod(setterName, valueType);
|
||||
|
||||
if (valueType.isPrimitive())
|
||||
{
|
||||
valueType = primitiveWrapperTypeMap.get(valueType);
|
||||
}
|
||||
|
||||
if (supportedValueTypes.contains(valueType))
|
||||
{
|
||||
setterByConfigName.put(configName, setter);
|
||||
valueTypeByConfigName.put(configName, valueType);
|
||||
configNames.add(configName);
|
||||
}
|
||||
}
|
||||
catch (final NoSuchMethodException nsme)
|
||||
{
|
||||
LOGGER.warn("Cannot support Keycloak adapter config field {} as no appropriate setter {} could be found in {}",
|
||||
fieldName, setterName, cls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cls = cls.getSuperclass();
|
||||
}
|
||||
|
||||
SETTER_BY_CONFIG_NAME = Collections.unmodifiableMap(setterByConfigName);
|
||||
VALUE_TYPE_BY_CONFIG_NAME = Collections.unmodifiableMap(valueTypeByConfigName);
|
||||
CONFIG_NAMES = Collections.unmodifiableList(configNames);
|
||||
}
|
||||
|
||||
protected Properties propertiesSource;
|
||||
|
||||
protected String configPropertyPrefix;
|
||||
|
||||
protected String placeholderPrefix = PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_PREFIX;
|
||||
|
||||
protected String placeholderSuffix = PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_SUFFIX;
|
||||
|
||||
protected String valueSeparator = PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR;
|
||||
|
||||
protected PropertyPlaceholderHelper placeholderHelper;
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "propertiesSource", this.propertiesSource);
|
||||
PropertyCheck.mandatory(this, "propertyPrefix", this.configPropertyPrefix);
|
||||
|
||||
this.placeholderHelper = new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param propertiesSource
|
||||
* the propertiesSource to set
|
||||
*/
|
||||
public void setPropertiesSource(final Properties propertiesSource)
|
||||
{
|
||||
this.propertiesSource = propertiesSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param configPropertyPrefix
|
||||
* the configPropertyPrefix to set
|
||||
*/
|
||||
public void setConfigPropertyPrefix(final String configPropertyPrefix)
|
||||
{
|
||||
this.configPropertyPrefix = configPropertyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param placeholderPrefix
|
||||
* the placeholderPrefix to set
|
||||
*/
|
||||
public void setPlaceholderPrefix(final String placeholderPrefix)
|
||||
{
|
||||
this.placeholderPrefix = placeholderPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param placeholderSuffix
|
||||
* the placeholderSuffix to set
|
||||
*/
|
||||
public void setPlaceholderSuffix(final String placeholderSuffix)
|
||||
{
|
||||
this.placeholderSuffix = placeholderSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param valueSeparator
|
||||
* the valueSeparator to set
|
||||
*/
|
||||
public void setValueSeparator(final String valueSeparator)
|
||||
{
|
||||
this.valueSeparator = valueSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public AdapterConfig getObject() throws Exception
|
||||
{
|
||||
final AdapterConfig adapterConfig = new AdapterConfig();
|
||||
|
||||
CONFIG_NAMES.forEach(configFieldName -> {
|
||||
final Class<?> valueType = VALUE_TYPE_BY_CONFIG_NAME.get(configFieldName);
|
||||
|
||||
Object value;
|
||||
if (Map.class.isAssignableFrom(valueType))
|
||||
{
|
||||
value = this.loadConfigMap(configFieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = this.loadConfigValue(configFieldName, valueType);
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
LOGGER.debug("Loaded {} as value of adapter config field {}", value, configFieldName);
|
||||
try
|
||||
{
|
||||
final Method setter = SETTER_BY_CONFIG_NAME.get(configFieldName);
|
||||
setter.invoke(adapterConfig, value);
|
||||
}
|
||||
catch (final IllegalAccessException | InvocationTargetException ex)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Error building adapter configuration", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.trace("No value specified for adapter config field {}", configFieldName);
|
||||
}
|
||||
});
|
||||
|
||||
return adapterConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getObjectType()
|
||||
{
|
||||
return AdapterConfig.class;
|
||||
}
|
||||
|
||||
protected Object loadConfigValue(final String configFieldName, final Class<?> valueType)
|
||||
{
|
||||
Object effectiveValue;
|
||||
|
||||
final String propertyName = this.configPropertyPrefix + "." + configFieldName;
|
||||
String value = this.propertiesSource.getProperty(propertyName);
|
||||
if (value != null)
|
||||
{
|
||||
value = this.placeholderHelper.replacePlaceholders(value, this.propertiesSource);
|
||||
}
|
||||
|
||||
if (value != null && !value.trim().isEmpty())
|
||||
{
|
||||
final String trimmedValue = value.trim();
|
||||
if (Number.class.isAssignableFrom(valueType))
|
||||
{
|
||||
try
|
||||
{
|
||||
effectiveValue = valueType.getMethod("valueOf", String.class).invoke(null, trimmedValue);
|
||||
}
|
||||
catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException ex)
|
||||
{
|
||||
LOGGER.error(
|
||||
"Number-based value type {} does not provide a publicly accessible, static valueOf to handle conversion of value {}",
|
||||
valueType, trimmedValue);
|
||||
throw new AlfrescoRuntimeException("Failed to convert configuration value " + trimmedValue, ex);
|
||||
}
|
||||
}
|
||||
else if (Boolean.class.equals(valueType))
|
||||
{
|
||||
effectiveValue = Boolean.valueOf(trimmedValue);
|
||||
}
|
||||
else if (Character.class.equals(valueType))
|
||||
{
|
||||
if (trimmedValue.length() > 1)
|
||||
{
|
||||
throw new IllegalStateException("Value " + trimmedValue + " has more than one character");
|
||||
}
|
||||
effectiveValue = new Character(trimmedValue.charAt(0));
|
||||
}
|
||||
else if (String.class.equals(valueType))
|
||||
{
|
||||
effectiveValue = trimmedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnsupportedOperationException("Unsupported value type " + valueType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
effectiveValue = null;
|
||||
}
|
||||
|
||||
return effectiveValue;
|
||||
}
|
||||
|
||||
protected Map<String, Object> loadConfigMap(final String configFieldName)
|
||||
{
|
||||
final Map<String, Object> configMap = new HashMap<>();
|
||||
final String propertyPrefix = this.configPropertyPrefix + "." + configFieldName + ".";
|
||||
this.propertiesSource.stringPropertyNames().stream().filter(p -> p.startsWith(propertyPrefix)).forEach(propertyName -> {
|
||||
final String propertyConfigSuffix = propertyName.substring(propertyPrefix.length());
|
||||
String value = this.propertiesSource.getProperty(propertyName);
|
||||
value = this.placeholderHelper.replacePlaceholders(value, this.propertiesSource);
|
||||
|
||||
LOGGER.debug("Resolved value {} for map key {} of config field {}", value, propertyConfigSuffix, configFieldName);
|
||||
if (value != null && !value.trim().isEmpty())
|
||||
{
|
||||
configMap.put(propertyConfigSuffix, value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
return configMap.isEmpty() ? null : configMap;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2019 Acosix GmbH
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package de.acosix.alfresco.keycloak.repo.spring;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.alfresco.util.PropertyCheck;
|
||||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
/**
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class KeycloakDeploymentBeanFactory implements FactoryBean<KeycloakDeployment>, InitializingBean
|
||||
{
|
||||
|
||||
protected AdapterConfig adapterConfig;
|
||||
|
||||
protected int connectionTimeout;
|
||||
|
||||
protected int socketTimeout;
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet()
|
||||
{
|
||||
PropertyCheck.mandatory(this, "adapterConfig", this.adapterConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param adapterConfig
|
||||
* the adapterConfig to set
|
||||
*/
|
||||
public void setAdapterConfig(final AdapterConfig adapterConfig)
|
||||
{
|
||||
this.adapterConfig = adapterConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param connectionTimeout
|
||||
* the connectionTimeout to set
|
||||
*/
|
||||
public void setConnectionTimeout(final int connectionTimeout)
|
||||
{
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param socketTimeout
|
||||
* the socketTimeout to set
|
||||
*/
|
||||
public void setSocketTimeout(final int socketTimeout)
|
||||
{
|
||||
this.socketTimeout = socketTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public KeycloakDeployment getObject() throws Exception
|
||||
{
|
||||
final KeycloakDeployment keycloakDeployment = KeycloakDeploymentBuilder.build(this.adapterConfig);
|
||||
|
||||
HttpClientBuilder httpClientBuilder = new HttpClientBuilder();
|
||||
if (this.connectionTimeout > 0)
|
||||
{
|
||||
httpClientBuilder = httpClientBuilder.establishConnectionTimeout(this.connectionTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
if (this.socketTimeout > 0)
|
||||
{
|
||||
httpClientBuilder = httpClientBuilder.socketTimeout(this.socketTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
keycloakDeployment.setClient(httpClientBuilder.build(this.adapterConfig));
|
||||
|
||||
return keycloakDeployment;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isSingleton()
|
||||
{
|
||||
// individual components may need to modify its configuration for their specific use case
|
||||
// so this should not be a shared singleton
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Class<?> getObjectType()
|
||||
{
|
||||
return KeycloakDeployment.class;
|
||||
}
|
||||
}
|
11
repository/src/test/docker/Repository-Dockerfile
Normal file
11
repository/src/test/docker/Repository-Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM ${docker.tests.repositoryBaseImage}
|
||||
COPY maven ${docker.tests.repositoryWebappPath}
|
||||
|
||||
# merge additions to alfresco-global.properties
|
||||
RUN echo "" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||
&& echo "#MergeGlobalProperties" >> ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||
&& sed -i '/#MergeGlobalProperties/r ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/alfresco-global.addition.properties' ${docker.tests.repositoryWebappPath}/../../shared/classes/alfresco-global.properties \
|
||||
&& mv ${docker.tests.repositoryWebappPath}/WEB-INF/classes/alfresco/extension/entrypoint.sh $CATALINA_HOME/bin/ \
|
||||
&& chmod +x $CATALINA_HOME/bin/entrypoint.sh
|
||||
|
||||
CMD ["entrypoint.sh", "catalina.sh run -security"]
|
@ -0,0 +1,25 @@
|
||||
#
|
||||
# Copyright 2019 Acosix GmbH
|
||||
#
|
||||
# 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.
|
||||
|
||||
# note: this file is not named alfresco-global.properties to not override the default file in the image
|
||||
# instead it relies on Dockerfile post-processing to merge with the default file
|
||||
|
||||
authentication.chain=keycloak1:keycloak,alfrescoNtlm1:alfrescoNtlm
|
||||
|
||||
keycloak.adapter.auth-server-url=http://${docker.tests.host.name}:${docker.tests.keycloakPort}/auth
|
||||
keycloak.adapter.realm=test
|
||||
keycloak.adapter.resource=alfresco
|
||||
keycloak.adapter.credentials.provider=secret
|
||||
keycloak.adapter.credentials.secret=6f70a28f-98cd-41ca-8f2f-368a8797d708
|
@ -0,0 +1,25 @@
|
||||
#
|
||||
# Copyright 2019 Acosix GmbH
|
||||
#
|
||||
# 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.
|
||||
|
||||
log4j.rootLogger=error, File
|
||||
|
||||
log4j.appender.File=org.apache.log4j.DailyRollingFileAppender
|
||||
log4j.appender.File.File=\${catalina.base}/logs/alfresco.log
|
||||
log4j.appender.File.Append=true
|
||||
log4j.appender.File.DatePattern='.'yyyy-MM-dd
|
||||
log4j.appender.File.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.File.layout.ConversionPattern=%d{ISO8601} %-5p [%c] [%t] %m%n
|
||||
|
||||
log4j.logger.${project.artifactId}=DEBUG
|
10
repository/src/test/docker/alfresco/extension/entrypoint.sh
Normal file
10
repository/src/test/docker/alfresco/extension/entrypoint.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
ip=`hostname -I | awk '{print $1}'`
|
||||
hostip=`echo "${ip}" | sed -E 's/([0-9]+\.[0-9]+)\.0\.[0-9]+/\1.0.1/'`
|
||||
hostname="${DOCKER_HOST_NAME}"
|
||||
echo "${hostip} ${hostname}" >> /etc/hosts
|
||||
|
||||
bash -c "$@"
|
100
repository/src/test/docker/repository-it.xml
Normal file
100
repository/src/test/docker/repository-it.xml
Normal file
@ -0,0 +1,100 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!--
|
||||
Copyright 2019 Acosix GmbH
|
||||
|
||||
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.
|
||||
-->
|
||||
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>repository-it-docker</id>
|
||||
<formats>
|
||||
<format>dir</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.build.directory}</directory>
|
||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||
<includes>
|
||||
<include>${project.artifactId}-${project.version}-installable.jar</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>${project.basedir}/src/test/resources</directory>
|
||||
<outputDirectory>WEB-INF/classes</outputDirectory>
|
||||
<includes>
|
||||
<include>*.properties</include>
|
||||
<include>**/*.properties</include>
|
||||
</includes>
|
||||
<filtered>true</filtered>
|
||||
<lineEnding>lf</lineEnding>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||
<includes>
|
||||
<include>*</include>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>*.js</exclude>
|
||||
<exclude>**/*.js</exclude>
|
||||
<exclude>*.ftl</exclude>
|
||||
<exclude>**/*.ftl</exclude>
|
||||
<exclude>*.keystore</exclude>
|
||||
<exclude>**/*.keystore</exclude>
|
||||
</excludes>
|
||||
<filtered>true</filtered>
|
||||
<lineEnding>lf</lineEnding>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>${project.basedir}/src/test/docker/alfresco</directory>
|
||||
<outputDirectory>WEB-INF/classes/alfresco</outputDirectory>
|
||||
<includes>
|
||||
<include>*.js</include>
|
||||
<include>**/*.js</include>
|
||||
<include>*.ftl</include>
|
||||
<include>**/*.ftl</include>
|
||||
<include>*.keystore</include>
|
||||
<include>**/*.keystore</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
<dependencySets>
|
||||
<dependencySet>
|
||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||
<includes>
|
||||
<!-- everything else from Keycloak should already be bundled in Alfresco 6.x -->
|
||||
<include>org.keycloak:keycloak-servlet-filter-adapter:*</include>
|
||||
</includes>
|
||||
<scope>compile</scope>
|
||||
</dependencySet>
|
||||
<dependencySet>
|
||||
<outputDirectory>WEB-INF/lib</outputDirectory>
|
||||
<includes>
|
||||
<!-- TODO: Report bug against Maven PatternIncludesArtifactFilter#matchAgainst for incorrect return false-->
|
||||
<!-- when patterns with 5 tokens are listed in includes (like the installable JAR of Acosix Utility Core Repo), they may prevent evaluation of any additional patterns -->
|
||||
<!-- this cost me half a day to track down when the following three patterns were sorted last -->
|
||||
<include>org.orderofthebee.support-tools:*</include>
|
||||
<include>com.cronutils:*</include>
|
||||
<include>net.time4j:*</include>
|
||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.common:*</include>
|
||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz1:*</include>
|
||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
|
||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:*</include>
|
||||
</includes>
|
||||
<scope>test</scope>
|
||||
</dependencySet>
|
||||
</dependencySets>
|
||||
</assembly>
|
@ -0,0 +1 @@
|
||||
# only exists to ensure Maven creates path in project ./target
|
1342
repository/src/test/docker/test-realm.json
Normal file
1342
repository/src/test/docker/test-realm.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -306,11 +306,10 @@ public class KeycloakAdapterConfigElement extends BaseCustomConfigElement
|
||||
{
|
||||
for (final String configName : CONFIG_NAMES)
|
||||
{
|
||||
final Method setter = SETTER_BY_CONFIG_NAME.get(configName);
|
||||
|
||||
final Object value = this.configValueByField.get(configName);
|
||||
if (value != null)
|
||||
{
|
||||
final Method setter = SETTER_BY_CONFIG_NAME.get(configName);
|
||||
setter.invoke(config, value);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ public class KeycloakAdapterConfigElementReader implements ConfigElementReader
|
||||
configElement.setFieldValue(subElementName,
|
||||
valueType.getMethod("valueOf", String.class).invoke(null, textTrim));
|
||||
}
|
||||
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex)
|
||||
catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException ex)
|
||||
{
|
||||
LOGGER.error(
|
||||
"Number-based value type {} does not provide a publicly accessible, static valueOf to handle conversion of value {}",
|
||||
|
@ -19,6 +19,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@ -549,7 +550,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
||||
protected void prepareLoginFormEnhancement(final ServletContext context, final HttpServletRequest req, final HttpServletResponse res,
|
||||
final FilterRequestAuthenticator authenticator)
|
||||
{
|
||||
final RedirectCaptureServletHttpFacade captureFacade = new RedirectCaptureServletHttpFacade(req);
|
||||
final ResponseHeaderCookieCaptureServletHttpFacade captureFacade = new ResponseHeaderCookieCaptureServletHttpFacade(req);
|
||||
|
||||
authenticator.getChallenge().challenge(captureFacade);
|
||||
|
||||
@ -603,7 +604,7 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
||||
|
||||
};
|
||||
|
||||
final RedirectCaptureServletHttpFacade captureFacade = new RedirectCaptureServletHttpFacade(wrappedReq);
|
||||
final ResponseHeaderCookieCaptureServletHttpFacade captureFacade = new ResponseHeaderCookieCaptureServletHttpFacade(wrappedReq);
|
||||
|
||||
final OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(req, captureFacade,
|
||||
bodyBufferLimit != null ? bodyBufferLimit.intValue() : DEFAULT_BODY_BUFFER_LIMIT, this.keycloakDeployment, null);
|
||||
@ -786,6 +787,8 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
||||
{
|
||||
boolean skip = false;
|
||||
|
||||
final String authHeader = req.getHeader(HEADER_AUTHORIZATION);
|
||||
|
||||
final String servletPath = req.getServletPath();
|
||||
final String pathInfo = req.getPathInfo();
|
||||
final String servletRequestUri = servletPath + (pathInfo != null ? pathInfo : "");
|
||||
@ -824,24 +827,25 @@ public class KeycloakAuthenticationFilter implements DependencyInjectedFilter, I
|
||||
LOGGER.debug(
|
||||
"Explicitly not skipping doFilter as state and code query parameters of OAuth2 redirect as well as state cookie are present");
|
||||
}
|
||||
else if (req.getHeader(HEADER_AUTHORIZATION) != null && req.getHeader(HEADER_AUTHORIZATION).startsWith("Bearer "))
|
||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer "))
|
||||
{
|
||||
LOGGER.debug("Explicitly not skipping doFilter as Bearer authorization header is present");
|
||||
}
|
||||
else if (req.getHeader(HEADER_AUTHORIZATION) != null)
|
||||
else if (authHeader != null && authHeader.toLowerCase(Locale.ENGLISH).startsWith("basic "))
|
||||
{
|
||||
LOGGER.debug("Skipping doFilter as non-OIDC authorization header is present");
|
||||
LOGGER.debug("Explicitly not skipping doFilter as Basic authorization header is present");
|
||||
}
|
||||
else if (authHeader != null)
|
||||
{
|
||||
LOGGER.debug("Skipping doFilter as non-OIDC / non-Basic authorization header is present");
|
||||
skip = true;
|
||||
}
|
||||
else if (req.getHeader(HEADER_AUTHORIZATION) == null && (currentSession != null && AuthenticationUtil.isAuthenticated(req)))
|
||||
else if (currentSession != null && AuthenticationUtil.isAuthenticated(req))
|
||||
{
|
||||
final String userId = AuthenticationUtil.getUserId(req);
|
||||
LOGGER.debug("Existing HTTP session is associated with user {}", userId);
|
||||
|
||||
final KeycloakAccount keycloakAccount = (KeycloakAccount) currentSession.getAttribute(KeycloakAccount.class.getName());
|
||||
if (keycloakAccount != null)
|
||||
{
|
||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, userId, keycloakAccount);
|
||||
skip = this.validateAndRefreshKeycloakAuthentication(req, res, AuthenticationUtil.getUserId(req), keycloakAccount);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -36,7 +36,7 @@ import org.keycloak.adapters.spi.HttpFacade;
|
||||
*
|
||||
* @author Axel Faust
|
||||
*/
|
||||
public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
public class ResponseHeaderCookieCaptureServletHttpFacade extends ServletHttpFacade
|
||||
{
|
||||
|
||||
protected final Map<Pair<String, String>, javax.servlet.http.Cookie> cookies = new HashMap<>();
|
||||
@ -49,7 +49,7 @@ public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
* @param request
|
||||
* the servlet request to facade
|
||||
*/
|
||||
public RedirectCaptureServletHttpFacade(final HttpServletRequest request)
|
||||
public ResponseHeaderCookieCaptureServletHttpFacade(final HttpServletRequest request)
|
||||
{
|
||||
super(request, null);
|
||||
}
|
||||
@ -106,7 +106,7 @@ public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
@Override
|
||||
public void addHeader(final String name, final String value)
|
||||
{
|
||||
RedirectCaptureServletHttpFacade.this.headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value);
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,7 +116,7 @@ public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
@Override
|
||||
public void setHeader(final String name, final String value)
|
||||
{
|
||||
RedirectCaptureServletHttpFacade.this.headers.put(name, new ArrayList<>(Collections.singleton(value)));
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.headers.put(name, new ArrayList<>(Collections.singleton(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,7 +126,7 @@ public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
@Override
|
||||
public void resetCookie(final String name, final String path)
|
||||
{
|
||||
RedirectCaptureServletHttpFacade.this.cookies.remove(new Pair<>(name, path));
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.cookies.remove(new Pair<>(name, path));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,7 +146,7 @@ public class RedirectCaptureServletHttpFacade extends ServletHttpFacade
|
||||
cookie.setMaxAge(maxAge);
|
||||
cookie.setSecure(secure);
|
||||
cookie.setHttpOnly(httpOnly);
|
||||
RedirectCaptureServletHttpFacade.this.cookies.put(new Pair<>(name, path), cookie);
|
||||
ResponseHeaderCookieCaptureServletHttpFacade.this.cookies.put(new Pair<>(name, path), cookie);
|
||||
}
|
||||
|
||||
/**
|
Loading…
x
Reference in New Issue
Block a user