Fix token handling due web script remote re-auth

- RemoteUserAuthenticator may re-run KeycloakRemoteUserMapper for Bearer
  authentication
- RemoteUserAuthenticator simply re-sets current user without running
  through regular ticket validation hoops (which we already covered)
- need authentication listener to hook into triggered event for
  re-processing access token
- this adds hard-dependency to full acosix-utility module, which is
  extremely unfortunate - TODO: Move authenticator listener patch
  (enabling multiple listeners) into utility core, since it can be
  reasonably considered a non-invasive, baseline patch (does not alter
  core behaviours) relevant for potentially multiple extensions, which
  should not necessitate dependency on full utility module with its
  accompanying set of (more or less) invasive patches
This commit is contained in:
AFaust 2020-02-17 10:43:42 +01:00
parent 9d9f665f29
commit b926431d68
9 changed files with 265 additions and 3 deletions

View File

@ -5,4 +5,4 @@ module.version=${noSnapshotVersion}
module.repo.version.min=5
module.depends.acosix-utility-core=1.1.0-*
module.depends.acosix-utility=1.1.0-*

View File

@ -55,6 +55,115 @@
</property>
</bean>
<bean id="${moduleId}.authenticationListener" class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
<property name="applicationContextManager">
<ref bean="Authentication" />
</property>
<property name="interfaces">
<list>
<value>org.alfresco.repo.web.auth.AuthenticationListener</value>
</list>
</property>
<property name="sourceBeanName" value="${moduleId}.authenticationListener" />
<property name="defaultTarget">
<bean class="org.alfresco.repo.web.auth.NoopAuthenticationListener" />
</property>
</bean>
<bean class="de.acosix.alfresco.utility.common.spring.PropertyAlteringBeanDefinitionRegistryPostProcessor">
<property name="dependsOn">
<list>
<ref bean="acosix-utility.globalAuthenticationListener.listenersPatch" />
</list>
</property>
<property name="enabledPropertyKeys">
<list>
<value>acosix-utility.web.auth.multipleAuthenticationListeners.enabled</value>
</list>
</property>
<property name="propertiesSource" ref="global-properties" />
<property name="targetBeanName" value="globalAuthenticationListener" />
<property name="expectedClassName" value="de.acosix.alfresco.utility.repo.web.auth.AuthenticationListenersFacade" />
<property name="propertyName" value="authenticationListeners" />
<property name="beanReferenceNameList">
<list>
<idref bean="${moduleId}.authenticationListener" />
</list>
</property>
<property name="merge" value="true" />
</bean>
<bean class="de.acosix.alfresco.utility.common.spring.PropertyAlteringBeanDefinitionRegistryPostProcessor">
<property name="dependsOn">
<list>
<ref bean="acosix-utility.webDavAuthenticationListener.listenersPatch" />
</list>
</property>
<property name="enabledPropertyKeys">
<list>
<value>acosix-utility.web.auth.multipleAuthenticationListeners.enabled</value>
</list>
</property>
<property name="propertiesSource" ref="global-properties" />
<property name="targetBeanName" value="webDavAuthenticationListener" />
<property name="expectedClassName" value="de.acosix.alfresco.utility.repo.web.auth.AuthenticationListenersFacade" />
<property name="propertyName" value="authenticationListeners" />
<property name="beanReferenceNameList">
<list>
<idref bean="${moduleId}.authenticationListener" />
</list>
</property>
<property name="merge" value="true" />
</bean>
<bean class="de.acosix.alfresco.utility.common.spring.PropertyAlteringBeanDefinitionRegistryPostProcessor">
<property name="dependsOn">
<list>
<ref bean="acosix-utility.sharepointAuthenticationListener.listenersPatch" />
</list>
</property>
<property name="enabledPropertyKeys">
<list>
<value>acosix-utility.web.auth.multipleAuthenticationListeners.enabled</value>
</list>
</property>
<property name="propertiesSource" ref="global-properties" />
<property name="targetBeanName" value="sharepointAuthenticationListener" />
<property name="expectedClassName" value="de.acosix.alfresco.utility.repo.web.auth.AuthenticationListenersFacade" />
<!-- no longer exists in Alfresco 6.0 -->
<property name="failIfTargetBeanMissing" value="false" />
<property name="propertyName" value="authenticationListeners" />
<property name="beanReferenceNameList">
<list>
<idref bean="${moduleId}.authenticationListener" />
</list>
</property>
<property name="merge" value="true" />
</bean>
<bean class="de.acosix.alfresco.utility.common.spring.PropertyAlteringBeanDefinitionRegistryPostProcessor">
<property name="dependsOn">
<list>
<ref bean="acosix-utility.webScriptAuthenticationListener.listenersPatch" />
</list>
</property>
<property name="enabledPropertyKeys">
<list>
<value>acosix-utility.web.auth.multipleAuthenticationListeners.enabled</value>
</list>
</property>
<property name="propertiesSource" ref="global-properties" />
<property name="targetBeanName" value="webScriptAuthenticationListener" />
<property name="expectedClassName" value="de.acosix.alfresco.utility.repo.web.auth.AuthenticationListenersFacade" />
<property name="propertyName" value="authenticationListeners" />
<property name="beanReferenceNameList">
<list>
<idref bean="${moduleId}.authenticationListener" />
</list>
</property>
<property name="merge" value="true" />
</bean>
<bean name="${moduleId}.ssoToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
</bean>

View File

@ -148,6 +148,12 @@
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
</bean>
<bean id="${moduleId}.authenticationListener" class="${project.artifactId}.authentication.KeycloakAuthenticationListener">
<property name="authenticationService" ref="AuthenticationService" />
<property name="keycloakAuthenticationComponent" ref="authenticationComponent" />
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
</bean>
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
<property name="deployment" ref="keycloakDeployment" />
<property name="userName" value="${keycloak.synchronization.user}" />

View File

@ -292,6 +292,8 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
if (result != null || ticketToken.isActive())
{
// this may be triggered later via KeycloakAuthenticationListener anyway but since Alfresco is inconsistent about when
// AuthenticationListener's are called, do it manually
this.handleUserTokens(result != null ? result.getAccessToken() : ticketToken.getAccessToken(),
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
}

View File

@ -492,7 +492,6 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, keycloakSecurityContext.getIdToken(), true);
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
// store tokens in cache as well for ticket validation
@ -667,6 +666,15 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
"Skipping processKeycloakAuthenticationAndActions as Bearer authorization header for {} has already been processed by remote user mapper",
AlfrescoCompatibilityUtil.maskUsername(accessToken.getPreferredUsername()));
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, session.isNew());
// sessionUser should be guaranteed here, but still check - we need it for the cache key
if (sessionUser != null)
{
final String bearerToken = authHeader.substring("bearer ".length());
this.keycloakTicketTokenCache.put(sessionUser.getTicket(),
new RefreshableAccessTokenHolder(accessToken, accessToken, bearerToken, null));
}
skip = true;
}
else
@ -719,6 +727,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
LOGGER.trace(
"Skipping processKeycloakAuthenticationAndActions as access token in session from previous Bearer authorization for {} is still valid",
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
// accessToken may have already been handled by getSessionUser(), but don't count on it
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, false);
skip = true;
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2019 - 2020 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.cache.SimpleCache;
import org.alfresco.repo.web.auth.AuthenticationListener;
import org.alfresco.repo.web.auth.TicketCredentials;
import org.alfresco.repo.web.auth.WebCredentials;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.util.PropertyCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import de.acosix.alfresco.keycloak.repo.util.AlfrescoCompatibilityUtil;
import de.acosix.alfresco.keycloak.repo.util.RefreshableAccessTokenHolder;
/**
* This class provides a central listener for ticket-based authentications to ensure that any ticket-associated Keycloak access tokens are
* processed. This is made necessary by the fact that the Alfresco web script framework may discard any global authentication and
* re-authenticate the user by validating the ticket in the HTTP session, thus losing any effect of the access token processing in the
* global authentication. Additionally, in some cases the web script framework does not even check the authenticated session user and just
* sets a remotely authenticated user as the current user - in that case at least it informs of a pseudo-ticket-based authentication, which
* - due to Alfresco standard behaviour of one-ticket-per-user - reuses the same ticket the user already had been assigned.
*
* In short, Alfresco authentication is extremely inconsistent and this listener class helps to plug one more hole.
*
* @author Axel Faust
*/
public class KeycloakAuthenticationListener implements InitializingBean, AuthenticationListener
{
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthenticationListener.class);
protected AuthenticationService authenticationService;
protected KeycloakAuthenticationComponent keycloakAuthenticationComponent;
protected SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache;
/**
*
* {@inheritDoc}
*/
@Override
public void afterPropertiesSet()
{
PropertyCheck.mandatory(this, "authenticationService", this.authenticationService);
PropertyCheck.mandatory(this, "keycloakAuthenticationCompoennt", this.keycloakAuthenticationComponent);
PropertyCheck.mandatory(this, "keycloakTicketTokenCache", this.keycloakTicketTokenCache);
}
/**
*
* {@inheritDoc}
*/
@Override
public void userAuthenticated(final WebCredentials credentials)
{
if (credentials instanceof TicketCredentials)
{
// for whatever reason, the credentials don't expose the ticket
final String ticket = this.authenticationService.getCurrentTicket();
if (this.keycloakTicketTokenCache.contains(ticket))
{
final RefreshableAccessTokenHolder token = this.keycloakTicketTokenCache.get(ticket);
LOGGER.debug("Processing access token for {} after ticket-based authentication",
AlfrescoCompatibilityUtil.maskUsername(token.getAccessToken().getPreferredUsername()));
// any ticket-based authentication is not a fresh login as it reuses obtained authentications
this.keycloakAuthenticationComponent.handleUserTokens(token.getAccessToken(), token.getIdToken(), false);
}
}
}
/**
*
* {@inheritDoc}
*/
@Override
public void authenticationFailed(final WebCredentials credentials, final Exception ex)
{
// NO-OP
}
/**
*
* {@inheritDoc}
*/
@Override
public void authenticationFailed(final WebCredentials credentials)
{
// NO-OP
}
/**
* @param authenticationService
* the authenticationService to set
*/
public void setAuthenticationService(final AuthenticationService authenticationService)
{
this.authenticationService = authenticationService;
}
/**
* @param keycloakAuthenticationComponent
* the keycloakAuthenticationComponent to set
*/
public void setKeycloakAuthenticationComponent(final KeycloakAuthenticationComponent keycloakAuthenticationComponent)
{
this.keycloakAuthenticationComponent = keycloakAuthenticationComponent;
}
/**
* @param keycloakTicketTokenCache
* the keycloakTicketTokenCache to set
*/
public void setKeycloakTicketTokenCache(final SimpleCache<String, RefreshableAccessTokenHolder> keycloakTicketTokenCache)
{
this.keycloakTicketTokenCache = keycloakTicketTokenCache;
}
}

View File

@ -139,6 +139,7 @@ public class KeycloakRemoteUserMapper implements RemoteUserMapper, ActivateableB
() -> this.personService.personExists(preferredUsername) ? this.personService.getUserIdentifier(preferredUsername)
: preferredUsername);
// normally Alfresco masks user names in logging, but in this case it would run counter to the purpose of logging
LOGGER.debug("Authenticated user {} via bearer token, normalised as {}", preferredUsername, normalisedUserName);
remoteUser = normalisedUserName;

View File

@ -92,6 +92,7 @@
<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>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:*</include>
</includes>
<scope>test</scope>
</dependencySet>

View File

@ -70,7 +70,6 @@
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:*</include>
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:*</include>
<!-- full acosix-utility repo module required for extension to repository-tier permissions.post.json.js to take effect -->
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:*</include>
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include>
</includes>