mirror of
https://github.com/bmlong137/alfresco-keycloak.git
synced 2025-05-12 21:24:43 +00:00
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:
parent
9d9f665f29
commit
b926431d68
@ -5,4 +5,4 @@ module.version=${noSnapshotVersion}
|
|||||||
|
|
||||||
module.repo.version.min=5
|
module.repo.version.min=5
|
||||||
|
|
||||||
module.depends.acosix-utility-core=1.1.0-*
|
module.depends.acosix-utility=1.1.0-*
|
@ -55,6 +55,115 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</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">
|
<bean name="${moduleId}.ssoToSessionCache" factory-bean="cacheFactory" factory-method="createCache">
|
||||||
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
|
<constructor-arg value="cache.${moduleId}.ssoToSessionCache" />
|
||||||
</bean>
|
</bean>
|
||||||
|
@ -148,6 +148,12 @@
|
|||||||
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
|
<property name="keycloakTicketTokenCache" ref="${moduleId}-ticketTokenCache" />
|
||||||
</bean>
|
</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">
|
<bean id="idmClient" class="${project.artifactId}.client.IDMClientImpl">
|
||||||
<property name="deployment" ref="keycloakDeployment" />
|
<property name="deployment" ref="keycloakDeployment" />
|
||||||
<property name="userName" value="${keycloak.synchronization.user}" />
|
<property name="userName" value="${keycloak.synchronization.user}" />
|
||||||
|
@ -292,6 +292,8 @@ public class KeycloakAuthenticationComponent extends AbstractAuthenticationCompo
|
|||||||
|
|
||||||
if (result != null || ticketToken.isActive())
|
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(),
|
this.handleUserTokens(result != null ? result.getAccessToken() : ticketToken.getAccessToken(),
|
||||||
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
|
result != null ? result.getIdToken() : ticketToken.getIdToken(), false);
|
||||||
}
|
}
|
||||||
|
@ -492,7 +492,6 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
|
|
||||||
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
|
final SessionUser sessionUser = this.createUserEnvironment(session, userId);
|
||||||
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, keycloakSecurityContext.getIdToken(), true);
|
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, keycloakSecurityContext.getIdToken(), true);
|
||||||
|
|
||||||
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
|
this.authenticationListener.userAuthenticated(new KeycloakCredentials(accessToken));
|
||||||
|
|
||||||
// store tokens in cache as well for ticket validation
|
// 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",
|
"Skipping processKeycloakAuthenticationAndActions as Bearer authorization header for {} has already been processed by remote user mapper",
|
||||||
AlfrescoCompatibilityUtil.maskUsername(accessToken.getPreferredUsername()));
|
AlfrescoCompatibilityUtil.maskUsername(accessToken.getPreferredUsername()));
|
||||||
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, session.isNew());
|
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;
|
skip = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -719,6 +727,7 @@ public class KeycloakAuthenticationFilter extends BaseAuthenticationFilter
|
|||||||
LOGGER.trace(
|
LOGGER.trace(
|
||||||
"Skipping processKeycloakAuthenticationAndActions as access token in session from previous Bearer authorization for {} is still valid",
|
"Skipping processKeycloakAuthenticationAndActions as access token in session from previous Bearer authorization for {} is still valid",
|
||||||
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
|
AlfrescoCompatibilityUtil.maskUsername(sessionUser.getUserName()));
|
||||||
|
// accessToken may have already been handled by getSessionUser(), but don't count on it
|
||||||
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, false);
|
this.keycloakAuthenticationComponent.handleUserTokens(accessToken, accessToken, false);
|
||||||
skip = true;
|
skip = true;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -139,6 +139,7 @@ public class KeycloakRemoteUserMapper implements RemoteUserMapper, ActivateableB
|
|||||||
() -> this.personService.personExists(preferredUsername) ? this.personService.getUserIdentifier(preferredUsername)
|
() -> this.personService.personExists(preferredUsername) ? this.personService.getUserIdentifier(preferredUsername)
|
||||||
: 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);
|
LOGGER.debug("Authenticated user {} via bearer token, normalised as {}", preferredUsername, normalisedUserName);
|
||||||
|
|
||||||
remoteUser = normalisedUserName;
|
remoteUser = normalisedUserName;
|
||||||
|
@ -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.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.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.core.repo:jar:installable:*</include>
|
||||||
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:*</include>
|
||||||
</includes>
|
</includes>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependencySet>
|
</dependencySet>
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
|
<include>de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:*</include>
|
||||||
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:*</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>
|
<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>de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:*</include>
|
||||||
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include>
|
<include>${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*</include>
|
||||||
</includes>
|
</includes>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user