diff --git a/repository/module.properties b/repository/module.properties index 535f6c2..184584e 100644 --- a/repository/module.properties +++ b/repository/module.properties @@ -5,4 +5,4 @@ module.version=${noSnapshotVersion} module.repo.version.min=5 -module.depends.acosix-utility-core=1.1.0-* \ No newline at end of file +module.depends.acosix-utility=1.1.0-* \ No newline at end of file diff --git a/repository/src/main/config/module-context.xml b/repository/src/main/config/module-context.xml index 7155952..7327a0e 100644 --- a/repository/src/main/config/module-context.xml +++ b/repository/src/main/config/module-context.xml @@ -55,6 +55,115 @@ + + + + + + + org.alfresco.repo.web.auth.AuthenticationListener + + + + + + + + + + + + + + + + + acosix-utility.web.auth.multipleAuthenticationListeners.enabled + + + + + + + + + + + + + + + + + + + + + + + acosix-utility.web.auth.multipleAuthenticationListeners.enabled + + + + + + + + + + + + + + + + + + + + + + + acosix-utility.web.auth.multipleAuthenticationListeners.enabled + + + + + + + + + + + + + + + + + + + + + + + + + acosix-utility.web.auth.multipleAuthenticationListeners.enabled + + + + + + + + + + + + + + diff --git a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml index dbbdcf2..fd43f65 100644 --- a/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml +++ b/repository/src/main/globalConfig/subsystems/Authentication/keycloak/keycloak-authentication-context.xml @@ -148,6 +148,12 @@ + + + + + + diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java index 69b99f2..46d791a 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationComponent.java @@ -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); } diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java index 03113bf..786956f 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationFilter.java @@ -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; } diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationListener.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationListener.java new file mode 100644 index 0000000..a0c24c3 --- /dev/null +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakAuthenticationListener.java @@ -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 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 keycloakTicketTokenCache) + { + this.keycloakTicketTokenCache = keycloakTicketTokenCache; + } + +} diff --git a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java index 0e43390..8c68ebe 100644 --- a/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java +++ b/repository/src/main/java/de/acosix/alfresco/keycloak/repo/authentication/KeycloakRemoteUserMapper.java @@ -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; diff --git a/repository/src/test/docker/repository-it.xml b/repository/src/test/docker/repository-it.xml index 9bf5ec4..748fe4e 100644 --- a/repository/src/test/docker/repository-it.xml +++ b/repository/src/test/docker/repository-it.xml @@ -92,6 +92,7 @@ de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz1:* de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:* de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:* + de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:* test diff --git a/share/src/test/docker/repository-it.xml b/share/src/test/docker/repository-it.xml index 62e2e4c..12d24bd 100644 --- a/share/src/test/docker/repository-it.xml +++ b/share/src/test/docker/repository-it.xml @@ -70,7 +70,6 @@ de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo.quartz2:* ${project.groupId}:de.acosix.alfresco.keycloak.repo.deps:* de.acosix.alfresco.utility:de.acosix.alfresco.utility.core.repo:jar:installable:* - de.acosix.alfresco.utility:de.acosix.alfresco.utility.repo:jar:installable:* ${project.groupId}:de.acosix.alfresco.keycloak.repo:jar:installable:*