diff --git a/.secrets.baseline b/.secrets.baseline
index 9b8990346e..1283660722 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -1273,7 +1273,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false,
- "line_number": 770,
+ "line_number": 771,
"is_secret": false
}
],
diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java
index 46b8f65d3f..ac38d48543 100644
--- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java
+++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java
@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
- * Copyright (C) 2005 - 2023 Alfresco Software Limited
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -34,7 +34,7 @@ import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
+import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.web.auth.AuthenticationListener;
import org.alfresco.repo.web.auth.TicketCredentials;
@@ -55,7 +55,7 @@ import java.util.List;
import java.util.Set;
/**
- * Authenticator to provide Remote User based Header authentication dropping back to Basic Auth otherwise.
+ * Authenticator to provide Remote User based Header authentication dropping back to Basic Auth otherwise.
* Statelessly authenticating via a secure header now does not require a Session so can be used with
* request-level load balancers which was not previously possible.
*
@@ -73,9 +73,11 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
protected RemoteUserMapper remoteUserMapper;
protected AuthenticationComponent authenticationComponent;
- protected AdminConsoleAuthenticator adminConsoleAuthenticator;
+ protected ExternalUserAuthenticator adminConsoleAuthenticator;
+ protected ExternalUserAuthenticator webScriptsHomeAuthenticator;
private boolean alwaysAllowBasicAuthForAdminConsole = true;
+ private boolean alwaysAllowBasicAuthForWebScriptsHome = true;
List adminConsoleScriptFamilies;
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
@@ -84,7 +86,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
{
this.remoteUserMapper = remoteUserMapper;
}
-
+
public void setAuthenticationComponent(AuthenticationComponent authenticationComponent)
{
this.authenticationComponent = authenticationComponent;
@@ -100,6 +102,16 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
this.alwaysAllowBasicAuthForAdminConsole = alwaysAllowBasicAuthForAdminConsole;
}
+ public boolean isAlwaysAllowBasicAuthForWebScriptsHome()
+ {
+ return alwaysAllowBasicAuthForWebScriptsHome;
+ }
+
+ public void setAlwaysAllowBasicAuthForWebScriptsHome(boolean alwaysAllowBasicAuthForWebScriptsHome)
+ {
+ this.alwaysAllowBasicAuthForWebScriptsHome = alwaysAllowBasicAuthForWebScriptsHome;
+ }
+
public List getAdminConsoleScriptFamilies()
{
return adminConsoleScriptFamilies;
@@ -121,11 +133,17 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
}
public void setAdminConsoleAuthenticator(
- AdminConsoleAuthenticator adminConsoleAuthenticator)
+ ExternalUserAuthenticator adminConsoleAuthenticator)
{
this.adminConsoleAuthenticator = adminConsoleAuthenticator;
}
+ public void setWebScriptsHomeAuthenticator(
+ ExternalUserAuthenticator webScriptsHomeAuthenticator)
+ {
+ this.webScriptsHomeAuthenticator = webScriptsHomeAuthenticator;
+ }
+
@Override
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
{
@@ -139,11 +157,13 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
*/
public class RemoteUserAuthenticator extends BasicHttpAuthenticator
{
+ private static final String WEB_SCRIPTS_BASE_PATH = "org/springframework/extensions/webscripts";
+
public RemoteUserAuthenticator(WebScriptServletRequest req, WebScriptServletResponse res, AuthenticationListener listener)
{
super(req, res, listener);
}
-
+
@Override
public boolean authenticate(RequiredAuthentication required, boolean isGuest)
{
@@ -159,24 +179,47 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
{
if (servletReq.getServiceMatch() != null &&
- isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
+ isAdminConsole(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
{
userId = getAdminConsoleUser();
}
+ else if (servletReq.getServiceMatch() != null &&
+ isWebScriptsHome(servletReq.getServiceMatch().getWebScript()) && isWebScriptsHomeAuthenticatorActive())
+ {
+ userId = getWebScriptsHomeUser();
+ }
if (userId == null)
{
if (isAlwaysAllowBasicAuthForAdminConsole())
{
- final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
+ boolean shouldUseTimeout = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
- if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin())
+ if (shouldUseTimeout && isBasicAuthHeaderPresentForAdmin())
{
- return callBasicAuthForAdminConsoleAccess(required, isGuest);
+ return callBasicAuthForAdminConsoleOrWebScriptsHomeAccess(required, isGuest);
}
try
{
- userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole);
+ userId = getRemoteUserWithTimeout(shouldUseTimeout);
+ }
+ catch (AuthenticationTimeoutException e)
+ {
+ // return basic auth challenge
+ return false;
+ }
+ }
+ else if (isAlwaysAllowBasicAuthForWebScriptsHome())
+ {
+ boolean shouldUseTimeout = shouldUseTimeoutForAdminAccessingWebScriptsHome(required, isGuest);
+
+ if (shouldUseTimeout && isBasicAuthHeaderPresentForAdmin())
+ {
+ return callBasicAuthForAdminConsoleOrWebScriptsHomeAccess(required, isGuest);
+ }
+ try
+ {
+ userId = getRemoteUserWithTimeout(shouldUseTimeout);
}
catch (AuthenticationTimeoutException e)
{
@@ -255,38 +298,63 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
authenticated = super.authenticate(required, isGuest);
}
}
- if(!authenticated && servletReq.getServiceMatch() != null &&
- isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
+ if (!authenticated && servletReq.getServiceMatch() != null)
{
- adminConsoleAuthenticator.requestAuthentication(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
+ WebScript webScript = servletReq.getServiceMatch().getWebScript();
+
+ if (isAdminConsole(webScript) && isAdminConsoleAuthenticatorActive())
+ {
+ adminConsoleAuthenticator.requestAuthentication(
+ this.servletReq.getHttpServletRequest(),
+ this.servletRes.getHttpServletResponse());
+ }
+ else if (isWebScriptsHome(webScript)
+ && isWebScriptsHomeAuthenticatorActive())
+ {
+ webScriptsHomeAuthenticator.requestAuthentication(
+ this.servletReq.getHttpServletRequest(),
+ this.servletRes.getHttpServletResponse());
+ }
}
return authenticated;
}
- private boolean callBasicAuthForAdminConsoleAccess(RequiredAuthentication required, boolean isGuest)
+ private boolean callBasicAuthForAdminConsoleOrWebScriptsHomeAccess(RequiredAuthentication required, boolean isGuest)
{
// return REST call, after a timeout/basic auth challenge
if (LOGGER.isTraceEnabled())
{
- LOGGER.trace("An Admin Console request has come in with Basic Auth headers present for an admin user.");
+ LOGGER.trace("An Admin Console or WebScripts Home request has come in with Basic Auth headers present for an admin user.");
}
// In order to prompt for another password, in case it was not entered correctly,
// the output of this method should be returned by the calling "authenticate" method;
// This would also mean, that once the admin basic auth header is present,
- // the authentication chain will not be used for the admin console access
+ // the authentication chain will not be used for access
return super.authenticate(required, isGuest);
}
private boolean shouldUseTimeoutForAdminAccessingAdminConsole(RequiredAuthentication required, boolean isGuest)
{
- boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest &&
- servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript());
+ boolean adminConsoleTimeout = RequiredAuthentication.admin.equals(required) && !isGuest &&
+ servletReq.getServiceMatch() != null && isAdminConsole(servletReq.getServiceMatch().getWebScript());
if (LOGGER.isTraceEnabled())
{
- LOGGER.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole);
+ LOGGER.trace("Should ensure that the admins can login with basic auth: " + adminConsoleTimeout);
}
- return useTimeoutForAdminAccessingAdminConsole;
+ return adminConsoleTimeout;
+ }
+
+ private boolean shouldUseTimeoutForAdminAccessingWebScriptsHome(RequiredAuthentication required, boolean isGuest)
+ {
+ boolean adminWebScriptsHomeTimeout = RequiredAuthentication.admin.equals(required) && !isGuest &&
+ servletReq.getServiceMatch() != null && isWebScriptsHome(servletReq.getServiceMatch().getWebScript());
+
+ if (LOGGER.isTraceEnabled())
+ {
+ LOGGER.trace("Should ensure that the admins can login with basic auth: " + adminWebScriptsHomeTimeout);
+ }
+ return adminWebScriptsHomeTimeout;
}
private boolean isRemoteUserMapperActive()
@@ -299,7 +367,12 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return adminConsoleAuthenticator != null && (!(adminConsoleAuthenticator instanceof ActivateableBean) || ((ActivateableBean) adminConsoleAuthenticator).isActive());
}
- protected boolean isAdminConsoleWebScript(WebScript webScript)
+ private boolean isWebScriptsHomeAuthenticatorActive()
+ {
+ return webScriptsHomeAuthenticator != null && (!(webScriptsHomeAuthenticator instanceof ActivateableBean) || ((ActivateableBean) webScriptsHomeAuthenticator).isActive());
+ }
+
+ protected boolean isAdminConsole(WebScript webScript)
{
if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null
|| webScript.getDescription().getFamilys() == null)
@@ -313,7 +386,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
}
// intersect the "family" sets defined
- Set families = new HashSet(webScript.getDescription().getFamilys());
+ Set families = new HashSet<>(webScript.getDescription().getFamilys());
families.retainAll(adminConsoleScriptFamilies);
final boolean isAdminConsole = !families.isEmpty();
@@ -325,6 +398,23 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return isAdminConsole;
}
+ protected boolean isWebScriptsHome(WebScript webScript)
+ {
+ if (webScript == null || webScript.toString() == null)
+ {
+ return false;
+ }
+
+ boolean isWebScriptsHome = webScript.toString().startsWith(WEB_SCRIPTS_BASE_PATH);
+
+ if (LOGGER.isTraceEnabled() && isWebScriptsHome)
+ {
+ LOGGER.trace("Detected a WebScripts Home webscript: " + webScript);
+ }
+
+ return isWebScriptsHome;
+ }
+
protected String getRemoteUserWithTimeout(boolean useTimeout) throws AuthenticationTimeoutException
{
if (!useTimeout)
@@ -394,7 +484,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
protected String getRemoteUser()
{
String userId = null;
-
+
// If the remote user mapper is configured, we may be able to map in an externally authenticated user
if (isRemoteUserMapperActive())
{
@@ -423,7 +513,21 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
if (isRemoteUserMapperActive())
{
- userId = adminConsoleAuthenticator.getAdminConsoleUser(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
+ userId = adminConsoleAuthenticator.getUserId(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
+ }
+
+ logRemoteUserID(userId);
+
+ return userId;
+ }
+
+ protected String getWebScriptsHomeUser()
+ {
+ String userId = null;
+
+ if (isRemoteUserMapperActive())
+ {
+ userId = webScriptsHomeAuthenticator.getUserId(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
}
logRemoteUserID(userId);
diff --git a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml
index 5fdf6664b1..8354bd260c 100644
--- a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml
+++ b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml
@@ -5,4 +5,4 @@
guest
required
internal
-
\ No newline at end of file
+
diff --git a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml
index b1630853ec..1f5b354797 100644
--- a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml
+++ b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml
@@ -214,9 +214,13 @@
+
${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}
+
+ ${authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled}
+
${authentication.getRemoteUserTimeoutMilliseconds}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java
index d57f3f2012..d13f5e9e28 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java
@@ -30,12 +30,12 @@ import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean;
/**
- * A default {@link AdminConsoleAuthenticator} implementation. Returns null to request a basic auth challenge.
+ * A default {@link ExternalUserAuthenticator} implementation. Returns null to request a basic auth challenge.
*/
-public class DefaultAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
+public class DefaultAdminConsoleAuthenticator implements ExternalUserAuthenticator, ActivateableBean
{
@Override
- public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
+ public String getUserId(HttpServletRequest request, HttpServletResponse response)
{
return null;
}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java
new file mode 100644
index 0000000000..aa0b1bf858
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java
@@ -0,0 +1,55 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.external;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+
+/**
+ * A default {@link ExternalUserAuthenticator} implementation. Returns null to request a basic auth challenge.
+ */
+public class DefaultWebScriptsHomeAuthenticator implements ExternalUserAuthenticator, ActivateableBean
+{
+ @Override
+ public String getUserId(HttpServletRequest request, HttpServletResponse response)
+ {
+ return null;
+ }
+
+ @Override
+ public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
+ {
+ // No implementation
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return false;
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java
similarity index 72%
rename from repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java
rename to repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java
index 9ff97debbb..e6cfaf3674 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java
@@ -29,28 +29,17 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
- * An interface for objects capable of extracting an externally authenticated user ID from the HTTP Admin Console webscript request.
+ * An interface for objects capable of extracting an externally authenticated user ID from the HTTP request.
*/
-public interface AdminConsoleAuthenticator
+public interface ExternalUserAuthenticator
{
/**
- * Gets an externally authenticated user ID from the HTTP Admin Console webscript request.
+ * Gets an externally authenticated user ID from the HTTP request.
*
- * @param request
- * the request
- * @param response
- * the response
* @return the user ID or null
if the user is unauthenticated
*/
- String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response);
+ String getUserId(HttpServletRequest request, HttpServletResponse response);
- /**
- * Requests an authentication.
- *
- * @param request
- * the request
- * @param response
- * the response
- */
+ /* Sends redirect to external site to initiate the OIDC authorization code flow. */
void requestAuthentication(HttpServletRequest request, HttpServletResponse response);
}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
index 7593e982e9..8d0bef15c5 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java
@@ -76,6 +76,18 @@ public class IdentityServiceConfig
private String lastNameAttribute;
private String emailAttribute;
private long jwtClockSkewMs;
+ private String webScriptsHomeRedirectPath;
+ private String webScriptsHomeScopes;
+
+ public String getWebScriptsHomeRedirectPath()
+ {
+ return webScriptsHomeRedirectPath;
+ }
+
+ public void setWebScriptsHomeRedirectPath(String webScriptsHomeRedirectPath)
+ {
+ this.webScriptsHomeRedirectPath = webScriptsHomeRedirectPath;
+ }
/**
*
@@ -359,6 +371,18 @@ public class IdentityServiceConfig
this.adminConsoleScopes = adminConsoleScopes;
}
+ public Set getWebScriptsHomeScopes()
+ {
+ return Stream.of(webScriptsHomeScopes.split(","))
+ .map(String::trim)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public void setWebScriptsHomeScopes(String webScriptsHomeScopes)
+ {
+ this.webScriptsHomeScopes = webScriptsHomeScopes;
+ }
+
public Set getPasswordGrantScopes()
{
return Stream.of(passwordGrantScopes.split(","))
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java
similarity index 58%
rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java
rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java
index 8907c2f808..616ae6f34e 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED;
@@ -32,7 +32,6 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
-import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -50,9 +49,8 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.web.util.UriComponentsBuilder;
-import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
+import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
@@ -60,27 +58,26 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
-/**
- * An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow.
- */
-public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
+public abstract class AbstractIdentityServiceAuthenticator implements ExternalUserAuthenticator
{
- private static final Logger LOGGER = LoggerFactory.getLogger(IdentityServiceAdminConsoleAuthenticator.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIdentityServiceAuthenticator.class);
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
- private IdentityServiceConfig identityServiceConfig;
- private IdentityServiceFacade identityServiceFacade;
- private AdminConsoleAuthenticationCookiesService cookiesService;
- private RemoteUserMapper remoteUserMapper;
- private boolean isEnabled;
+ protected IdentityServiceConfig identityServiceConfig;
+ protected IdentityServiceFacade identityServiceFacade;
+ protected AdminAuthenticationCookiesService cookiesService;
+ protected RemoteUserMapper remoteUserMapper;
+
+ protected abstract String getConfiguredRedirectPath();
+
+ protected abstract Set getConfiguredScopes();
@Override
- public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
+ public String getUserId(HttpServletRequest request, HttpServletResponse response)
{
- // Try to extract username from the authorization header
String username = remoteUserMapper.getRemoteUser(request);
if (username != null)
{
@@ -107,16 +104,12 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
return null;
}
- return remoteUserMapper.getRemoteUser(decorateBearerHeader(bearerToken, request));
+ HttpServletRequest wrappedRequest = newRequestWrapper(Map.of("Authorization", "Bearer " + bearerToken), request);
+ return remoteUserMapper.getRemoteUser(wrappedRequest);
}
@Override
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
- {
- respondWithAuthChallenge(request, response);
- }
-
- private void respondWithAuthChallenge(HttpServletRequest request, HttpServletResponse response)
{
try
{
@@ -124,7 +117,8 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
{
LOGGER.debug("Responding with the authentication challenge");
}
- response.sendRedirect(getAuthenticationRequest(request));
+ String authenticationRequest = buildAuthRequestUrl(request);
+ response.sendRedirect(authenticationRequest);
}
catch (IOException e)
{
@@ -133,84 +127,34 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
}
}
- private String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code)
+ protected String getRedirectUri(String requestURL)
{
- String bearerToken = null;
- if (LOGGER.isDebugEnabled())
- {
- LOGGER.debug("Retrieving a response using the Authorization Code at the Token Endpoint");
- }
- try
- {
- AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
- authorizationCode(code, request.getRequestURL().toString()));
- addCookies(response, accessTokenAuthorization);
- bearerToken = accessTokenAuthorization.getAccessToken().getTokenValue();
- }
- catch (AuthorizationException exception)
- {
- if (LOGGER.isWarnEnabled())
- {
- LOGGER.warn(
- "Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
- exception.getMessage());
- }
- }
- return bearerToken;
+ return buildRedirectUri(requestURL, getConfiguredRedirectPath());
}
- private String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken)
- {
- String refreshToken = cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request);
- String authTokenExpiration = cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request);
- try
- {
- if (isAuthTokenExpired(authTokenExpiration))
- {
- bearerToken = refreshAuthToken(refreshToken, response);
- }
- }
- catch (Exception e)
- {
- if (LOGGER.isDebugEnabled())
- {
- LOGGER.debug("Error while trying to refresh Auth Token: {}", e.getMessage());
- }
- bearerToken = null;
- resetCookies(response);
- }
- return bearerToken;
- }
-
- private void addCookies(HttpServletResponse response, AccessTokenAuthorization accessTokenAuthorization)
- {
- cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken().getTokenValue(), response);
- cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(
- accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
- cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response);
- }
-
- private String getAuthenticationRequest(HttpServletRequest request)
+ public String buildAuthRequestUrl(HttpServletRequest request)
{
ClientRegistration clientRegistration = identityServiceFacade.getClientRegistration();
State state = new State();
- UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri())
+ UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails()
+ .getAuthorizationUri())
.queryParam("client_id", clientRegistration.getClientId())
.queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
.queryParam("response_type", "code")
- .queryParam("scope", String.join("+", getScopes(clientRegistration)))
+ .queryParam("scope", String.join("+", getConfiguredScopes(clientRegistration)))
.queryParam("state", state.toString());
if (StringUtils.isNotBlank(identityServiceConfig.getAudience()))
{
- authRequestBuilder.queryParam("audience", identityServiceConfig.getAudience());
+ builder.queryParam("audience", identityServiceConfig.getAudience());
}
- return authRequestBuilder.build().toUriString();
+ return builder.build()
+ .toUriString();
}
- private Set getScopes(ClientRegistration clientRegistration)
+ private Set getConfiguredScopes(ClientRegistration clientRegistration)
{
return Optional.ofNullable(clientRegistration.getProviderDetails())
.map(ProviderDetails::getConfigurationMetadata)
@@ -223,100 +167,149 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set getSupportedScopes(Scope scopes)
{
+ Set configuredScopes = getConfiguredScopes();
return scopes.stream()
- .filter(this::hasAdminConsoleScope)
.map(Identifier::getValue)
+ .filter(configuredScopes::contains)
.collect(Collectors.toSet());
}
- private boolean hasAdminConsoleScope(Scope.Value scope)
- {
- return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
- }
-
- private String getRedirectUri(String requestURL)
+ protected String buildRedirectUri(String requestURL, String overridePath)
{
try
{
URI originalUri = new URI(requestURL);
- URI redirectUri = new URI(originalUri.getScheme(), originalUri.getAuthority(), identityServiceConfig.getAdminConsoleRedirectPath(), originalUri.getQuery(), originalUri.getFragment());
+ String path = overridePath != null ? overridePath : originalUri.getPath();
+
+ URI redirectUri = new URI(
+ originalUri.getScheme(),
+ originalUri.getAuthority(),
+ path,
+ originalUri.getQuery(),
+ originalUri.getFragment());
+
return redirectUri.toASCIIString();
}
catch (URISyntaxException e)
{
- LOGGER.error("Error while trying to get the redirect URI and respond with the authentication challenge: {}", e.getMessage(), e);
+ LOGGER.error("Redirect URI construction failed: {}", e.getMessage(), e);
throw new AuthenticationException(e.getMessage(), e);
}
}
- private void resetCookies(HttpServletResponse response)
+ public void challenge(HttpServletRequest request, HttpServletResponse response)
+ {
+ try
+ {
+ response.sendRedirect(buildAuthRequestUrl(request));
+ }
+ catch (IOException e)
+ {
+ throw new AuthenticationException("Auth redirect failed", e);
+ }
+ }
+
+ protected String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code)
+ {
+ try
+ {
+ AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(authorizationCode(code, getRedirectUri(request.getRequestURL()
+ .toString())));
+ addCookies(response, accessTokenAuthorization);
+ return accessTokenAuthorization.getAccessToken()
+ .getTokenValue();
+ }
+ catch (AuthorizationException exception)
+ {
+ LOGGER.warn("Error while trying to retrieve token using Authorization Code: {}", exception.getMessage());
+ return null;
+ }
+ }
+
+ protected String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken)
+ {
+ String refreshToken = cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request);
+ String authTokenExpiration = cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request);
+
+ try
+ {
+ if (isAuthTokenExpired(authTokenExpiration))
+ {
+ bearerToken = refreshAuthToken(refreshToken, response);
+ }
+ }
+ catch (Exception e)
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("Token refresh failed: {}", e.getMessage());
+ }
+ bearerToken = null;
+ resetCookies(response);
+ }
+
+ return bearerToken;
+ }
+
+ private static boolean isAuthTokenExpired(String authTokenExpiration)
+ {
+ return authTokenExpiration == null || Instant.now()
+ .compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0;
+ }
+
+ private String refreshAuthToken(String refreshToken, HttpServletResponse response)
+ {
+ AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.refreshToken(refreshToken));
+ if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
+ {
+ throw new AuthenticationException("Refresh token response is invalid.");
+ }
+ addCookies(response, accessTokenAuthorization);
+ return accessTokenAuthorization.getAccessToken()
+ .getTokenValue();
+
+ }
+
+ protected void addCookies(HttpServletResponse response, AccessTokenAuthorization accessTokenAuthorization)
+ {
+ cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken()
+ .getTokenValue(), response);
+ cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(accessTokenAuthorization.getAccessToken()
+ .getExpiresAt()
+ .toEpochMilli()), response);
+ cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response);
+ }
+
+ protected void resetCookies(HttpServletResponse response)
{
cookiesService.resetCookie(ALFRESCO_TOKEN_EXPIRATION, response);
cookiesService.resetCookie(ALFRESCO_ACCESS_TOKEN, response);
cookiesService.resetCookie(ALFRESCO_REFRESH_TOKEN, response);
}
- private String refreshAuthToken(String refreshToken, HttpServletResponse response)
+ protected HttpServletRequest newRequestWrapper(Map headers, HttpServletRequest request)
{
- AccessTokenAuthorization accessTokenAuthorization = doRefreshAuthToken(refreshToken);
- addCookies(response, accessTokenAuthorization);
- return accessTokenAuthorization.getAccessToken().getTokenValue();
+ return new AdditionalHeadersHttpServletRequestWrapper(headers, request);
}
- private AccessTokenAuthorization doRefreshAuthToken(String refreshToken)
+ // Setters
+ public void setIdentityServiceConfig(IdentityServiceConfig config)
{
- AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
- AuthorizationGrant.refreshToken(refreshToken));
- if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
- {
- throw new AuthenticationException("AccessTokenResponse is null or empty");
- }
- return accessTokenAuthorization;
+ this.identityServiceConfig = config;
}
- private static boolean isAuthTokenExpired(String authTokenExpiration)
+ public void setIdentityServiceFacade(IdentityServiceFacade facade)
{
- return Instant.now().compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0;
+ this.identityServiceFacade = facade;
}
- private HttpServletRequest decorateBearerHeader(String authToken, HttpServletRequest servletRequest)
+ public void setCookiesService(AdminAuthenticationCookiesService service)
{
- Map additionalHeaders = new HashMap<>();
- additionalHeaders.put("Authorization", "Bearer " + authToken);
- return new AdminConsoleHttpServletRequestWrapper(additionalHeaders, servletRequest);
+ this.cookiesService = service;
}
- public void setIdentityServiceFacade(
- IdentityServiceFacade identityServiceFacade)
+ public void setRemoteUserMapper(RemoteUserMapper mapper)
{
- this.identityServiceFacade = identityServiceFacade;
- }
-
- public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
- {
- this.remoteUserMapper = remoteUserMapper;
- }
-
- public void setCookiesService(
- AdminConsoleAuthenticationCookiesService cookiesService)
- {
- this.cookiesService = cookiesService;
- }
-
- public void setIdentityServiceConfig(
- IdentityServiceConfig identityServiceConfig)
- {
- this.identityServiceConfig = identityServiceConfig;
- }
-
- @Override
- public boolean isActive()
- {
- return this.isEnabled;
- }
-
- public void setActive(boolean isEnabled)
- {
- this.isEnabled = isEnabled;
+ this.remoteUserMapper = mapper;
}
}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java
similarity index 87%
rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java
rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java
index d80eca391a..8d4ada3eaa 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication;
import static java.util.Arrays.asList;
import static java.util.Collections.enumeration;
@@ -37,18 +37,12 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.alfresco.util.PropertyCheck;
-public class AdminConsoleHttpServletRequestWrapper extends HttpServletRequestWrapper
+public class AdditionalHeadersHttpServletRequestWrapper extends HttpServletRequestWrapper
{
private final Map additionalHeaders;
private final HttpServletRequest wrappedRequest;
- /**
- * Constructs a request object wrapping the given request.
- *
- * @param request the request to wrap
- * @throws IllegalArgumentException if the request is null
- */
- public AdminConsoleHttpServletRequestWrapper(Map additionalHeaders, HttpServletRequest request)
+ public AdditionalHeadersHttpServletRequestWrapper(Map additionalHeaders, HttpServletRequest request)
{
super(request);
PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders);
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java
similarity index 95%
rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java
rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java
index b01dbd4297..afa0d56457 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
@@ -33,12 +33,12 @@ import org.alfresco.repo.admin.SysAdminParams;
/**
* Service to handle Admin Console authentication-related cookies.
*/
-public class AdminConsoleAuthenticationCookiesService
+public class AdminAuthenticationCookiesService
{
private final SysAdminParams sysAdminParams;
private final int cookieLifetime;
- public AdminConsoleAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime)
+ public AdminAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime)
{
this.sysAdminParams = sysAdminParams;
this.cookieLifetime = cookieLifetime;
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java
new file mode 100644
index 0000000000..3d06023da6
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java
@@ -0,0 +1,64 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.authentication.admin;
+
+import java.util.Set;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AbstractIdentityServiceAuthenticator;
+
+/**
+ * An {@link ExternalUserAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow.
+ */
+public class IdentityServiceAdminConsoleAuthenticator extends AbstractIdentityServiceAuthenticator
+ implements ExternalUserAuthenticator, ActivateableBean
+{
+ private boolean isEnabled;
+
+ @Override
+ protected Set getConfiguredScopes()
+ {
+ return identityServiceConfig.getAdminConsoleScopes();
+ }
+
+ @Override
+ protected String getConfiguredRedirectPath()
+ {
+ return identityServiceConfig.getAdminConsoleRedirectPath();
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return isEnabled;
+ }
+
+ public void setActive(boolean isEnabled)
+ {
+ this.isEnabled = isEnabled;
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java
new file mode 100644
index 0000000000..030fd912ed
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java
@@ -0,0 +1,64 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.authentication.webscripts;
+
+import java.util.Set;
+
+import org.alfresco.repo.management.subsystems.ActivateableBean;
+import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AbstractIdentityServiceAuthenticator;
+
+/**
+ * An {@link ExternalUserAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow.
+ */
+public class IdentityServiceWebScriptsHomeAuthenticator extends AbstractIdentityServiceAuthenticator
+ implements ExternalUserAuthenticator, ActivateableBean
+{
+ private boolean isEnabled;
+
+ @Override
+ protected String getConfiguredRedirectPath()
+ {
+ return identityServiceConfig.getWebScriptsHomeRedirectPath();
+ }
+
+ @Override
+ protected Set getConfiguredScopes()
+ {
+ return identityServiceConfig.getWebScriptsHomeScopes();
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return this.isEnabled;
+ }
+
+ public void setActive(boolean isEnabled)
+ {
+ this.isEnabled = isEnabled;
+ }
+}
diff --git a/repository/src/main/resources/alfresco/authentication-services-context.xml b/repository/src/main/resources/alfresco/authentication-services-context.xml
index 7d4f6d9666..4a31cd1d06 100644
--- a/repository/src/main/resources/alfresco/authentication-services-context.xml
+++ b/repository/src/main/resources/alfresco/authentication-services-context.xml
@@ -135,7 +135,7 @@
- org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator
+ org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator
org.alfresco.repo.management.subsystems.ActivateableBean
@@ -144,6 +144,22 @@
+
+
+
+
+
+
+ org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator
+ org.alfresco.repo.management.subsystems.ActivateableBean
+
+
+
+ webScriptsHomeAuthenticator
+
+
+
diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties
index 0431d844d3..6255bc31db 100644
--- a/repository/src/main/resources/alfresco/repository.properties
+++ b/repository/src/main/resources/alfresco/repository.properties
@@ -563,6 +563,7 @@ authentication.ticket.validDuration=PT1H
authentication.ticket.useSingleTicketPerUser=true
authentication.alwaysAllowBasicAuthForAdminConsole.enabled=true
+authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled=true
authentication.getRemoteUserTimeoutMilliseconds=10000
# FTP access
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml
index e25132c527..824c4ac686 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml
@@ -104,4 +104,7 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
index 8bc16b3c06..9416715bcd 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
@@ -170,6 +170,9 @@
${identity-service.admin-console.scopes:openid,profile,email,offline_access}
+
+ ${identity-service.webscripts-home.scopes:openid,profile,email,offline_access}
+
${identity-service.password-grant.scopes:openid,profile,email}
@@ -179,6 +182,9 @@
${identity-service.jwt-clock-skew-ms:0}
+
+ ${identity-service.webscripts-home.redirect-path}
+
@@ -197,12 +203,12 @@
-
-
-
-
+
+
+
+
-
+
${identity-service.authentication.enabled}
@@ -210,7 +216,7 @@
-
+
@@ -220,6 +226,24 @@
+
+
+ ${identity-service.authentication.enabled}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
index e6d517c1ad..d130e4e89d 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties
@@ -12,11 +12,13 @@ identity-service.resource=alfresco
identity-service.credentials.secret=
identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
+identity-service.webscripts-home.redirect-path=/alfresco/s/index
identity-service.signature-algorithms=RS256,PS256
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.admin-console.scopes=openid,profile,email,offline_access
+identity-service.webscripts-home.scopes=openid,profile,email,offline_access
identity-service.password-grant.scopes=openid,profile,email
identity-service.issuer-attribute=issuer
identity-service.jwt-clock-skew-ms=0
diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
index 0a4e6733cf..d3214d7bcb 100644
--- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
+++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
@@ -34,11 +34,12 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
-import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
-import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
-import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapperUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesServiceUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.authentication.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest;
+import org.alfresco.repo.security.authentication.identityservice.webscript.IdentityServiceWebScriptsHomeAuthenticatorUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
@@ -153,9 +154,10 @@ import org.alfresco.util.testing.category.NonBuildTests;
IdentityServiceJITProvisioningHandlerUnitTest.class,
AccessTokenToDecodedTokenUserMapperUnitTest.class,
TokenUserToOIDCUserMapperUnitTest.class,
- AdminConsoleAuthenticationCookiesServiceUnitTest.class,
- AdminConsoleHttpServletRequestWrapperUnitTest.class,
+ AdminAuthenticationCookiesServiceUnitTest.class,
+ AdditionalHeadersHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
+ IdentityServiceWebScriptsHomeAuthenticatorUnitTest.class,
ClientRegistrationProviderUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java
similarity index 89%
rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java
rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java
index 9b470d50f5..546bd56f06 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication;
import static java.util.Collections.enumeration;
import static java.util.Collections.list;
@@ -40,51 +40,45 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
import jakarta.servlet.http.HttpServletRequest;
-import org.alfresco.error.AlfrescoRuntimeException;
+
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
-@SuppressWarnings("PMD.UseDiamondOperator")
-public class AdminConsoleHttpServletRequestWrapperUnitTest
-{
+import org.alfresco.error.AlfrescoRuntimeException;
+@SuppressWarnings("PMD.UseDiamondOperator")
+public class AdditionalHeadersHttpServletRequestWrapperUnitTest
+{
private static final String DEFAULT_HEADER = "default_header";
private static final String DEFAULT_HEADER_VALUE = "default_value";
private static final String ADDITIONAL_HEADER = "additional_header";
private static final String ADDITIONAL_HEADER_VALUE = "additional_value";
- private static final Map DEFAULT_HEADERS = new HashMap()
- {{
- put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE);
- }};
- private static final Map ADDITIONAL_HEADERS = new HashMap()
- {{
- put(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE);
- }};
+ private static final Map DEFAULT_HEADERS = Map.of(DEFAULT_HEADER, DEFAULT_HEADER_VALUE);
+ private static final Map ADDITIONAL_HEADERS = Map.of(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE);
@Mock
private HttpServletRequest request;
- private AdminConsoleHttpServletRequestWrapper requestWrapper;
+ private AdditionalHeadersHttpServletRequestWrapper requestWrapper;
@Before
public void setUp()
{
initMocks(this);
- requestWrapper = new AdminConsoleHttpServletRequestWrapper(ADDITIONAL_HEADERS, request);
+ requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(ADDITIONAL_HEADERS, request);
}
@Test(expected = AlfrescoRuntimeException.class)
public void wrapperShouldNotBeInstancedWithoutAdditionalHeaders()
{
- new AdminConsoleHttpServletRequestWrapper(null, request);
+ new AdditionalHeadersHttpServletRequestWrapper(null, request);
}
@Test(expected = IllegalArgumentException.class)
public void wrapperShouldNotBeInstancedWithoutRequestsToWrap()
{
- new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), null);
+ new AdditionalHeadersHttpServletRequestWrapper(new HashMap<>(), null);
}
@Test
@@ -109,7 +103,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
{
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
- requestWrapper = new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), request);
+ requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(new HashMap<>(), request);
Enumeration headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
@@ -161,7 +155,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
- requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
+ requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request);
String header = requestWrapper.getHeader(DEFAULT_HEADER);
assertEquals("The header should have the overridden value", overrideHeaderValue, header);
@@ -201,7 +195,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
- requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
+ requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request);
Enumeration headers = requestWrapper.getHeaders(DEFAULT_HEADER);
assertNotNull("The headers enumeration should not be null", headers);
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java
similarity index 94%
rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java
rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java
index e5c0b8cd06..3add1d0934 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -37,14 +37,16 @@ import static org.mockito.MockitoAnnotations.initMocks;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.alfresco.repo.admin.SysAdminParams;
+
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
-public class AdminConsoleAuthenticationCookiesServiceUnitTest
+import org.alfresco.repo.admin.SysAdminParams;
+
+public class AdminAuthenticationCookiesServiceUnitTest
{
private static final int DEFAULT_COOKIE_LIFETIME = 86400;
private static final String COOKIE_NAME = "cookie";
@@ -57,13 +59,13 @@ public class AdminConsoleAuthenticationCookiesServiceUnitTest
private SysAdminParams sysAdminParams;
@Captor
private ArgumentCaptor cookieCaptor;
- private AdminConsoleAuthenticationCookiesService cookiesService;
+ private AdminAuthenticationCookiesService cookiesService;
@Before
public void setUp()
{
initMocks(this);
- cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME);
+ cookiesService = new AdminAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME);
}
@Test
@@ -136,7 +138,7 @@ public class AdminConsoleAuthenticationCookiesServiceUnitTest
public void cookieWithCustomMaxAgeShouldBeAddedToTheResponse()
{
int customMaxAge = 60;
- cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, customMaxAge);
+ cookiesService = new AdminAuthenticationCookiesService(sysAdminParams, customMaxAge);
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
similarity index 94%
rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
index 6d1ca62de4..f0f5b4fc15 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java
@@ -23,7 +23,7 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.security.authentication.identityservice.admin;
+package org.alfresco.repo.security.authentication.identityservice.authentication.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -58,11 +58,12 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapper;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService;
@SuppressWarnings("PMD.AvoidStringBufferField")
public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
-
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
@@ -76,7 +77,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Mock
IdentityServiceConfig identityServiceConfig;
@Mock
- AdminConsoleAuthenticationCookiesService cookiesService;
+ AdminAuthenticationCookiesService cookiesService;
@Mock
RemoteUserMapper remoteUserMapper;
@Mock
@@ -84,7 +85,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Mock
AccessToken accessToken;
@Captor
- ArgumentCaptor requestCaptor;
+ ArgumentCaptor requestCaptor;
IdentityServiceAdminConsoleAuthenticator authenticator;
@@ -122,7 +123,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
- String username = authenticator.getAdminConsoleUser(request, response);
+ String username = authenticator.getUserId(request, response);
assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
assertEquals("admin", username);
@@ -143,7 +144,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
- String username = authenticator.getAdminConsoleUser(request, response);
+ String username = authenticator.getUserId(request, response);
verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
@@ -207,7 +208,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class);
- String username = authenticator.getAdminConsoleUser(request, response);
+ String username = authenticator.getUserId(request, response);
verify(cookiesService).resetCookie(ALFRESCO_ACCESS_TOKEN, response);
verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response);
@@ -228,7 +229,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
.thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
- String username = authenticator.getAdminConsoleUser(request, response);
+ String username = authenticator.getUserId(request, response);
verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
@@ -241,7 +242,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin");
- String username = authenticator.getAdminConsoleUser(request, response);
+ String username = authenticator.getUserId(request, response);
assertEquals("admin", username);
}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java
new file mode 100644
index 0000000000..ee68e528af
--- /dev/null
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java
@@ -0,0 +1,253 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2025 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.repo.security.authentication.identityservice.webscript;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import com.nimbusds.oauth2.sdk.Scope;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+
+import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapper;
+import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService;
+import org.alfresco.repo.security.authentication.identityservice.authentication.webscripts.IdentityServiceWebScriptsHomeAuthenticator;
+
+@SuppressWarnings("PMD.AvoidStringBufferField")
+public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest
+{
+
+ private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
+ private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
+ private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
+
+ @Mock
+ HttpServletRequest request;
+ @Mock
+ HttpServletResponse response;
+ @Mock
+ IdentityServiceFacade identityServiceFacade;
+ @Mock
+ IdentityServiceConfig identityServiceConfig;
+ @Mock
+ AdminAuthenticationCookiesService cookiesService;
+ @Mock
+ RemoteUserMapper remoteUserMapper;
+ @Mock
+ AccessTokenAuthorization accessTokenAuthorization;
+ @Mock
+ AccessToken accessToken;
+ @Captor
+ ArgumentCaptor requestCaptor;
+
+ IdentityServiceWebScriptsHomeAuthenticator authenticator;
+
+ StringBuffer webScriptHomeURL = new StringBuffer("http://localhost:8080/alfresco/s/index");
+
+ @Before
+ public void setup()
+ {
+ initMocks(this);
+ ClientRegistration clientRegistration = mock(ClientRegistration.class);
+ ProviderDetails providerDetails = mock(ProviderDetails.class);
+ Scope scope = Scope.parse(Arrays.asList("openid", "profile", "email", "offline_access"));
+
+ when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
+ when(clientRegistration.getClientId()).thenReturn("alfresco");
+ when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
+ when(providerDetails.getConfigurationMetadata()).thenReturn(Map.of("scopes_supported", scope));
+ when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
+ when(request.getRequestURL()).thenReturn(webScriptHomeURL);
+ when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
+
+ authenticator = new IdentityServiceWebScriptsHomeAuthenticator();
+ authenticator.setActive(true);
+ authenticator.setIdentityServiceFacade(identityServiceFacade);
+ authenticator.setCookiesService(cookiesService);
+ authenticator.setRemoteUserMapper(remoteUserMapper);
+ authenticator.setIdentityServiceConfig(identityServiceConfig);
+ }
+
+ @Test
+ public void shouldCallRemoteMapperIfTokenIsInCookies()
+ {
+ when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("JWT_TOKEN");
+ when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
+ String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
+ when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
+
+ String username = authenticator.getUserId(request, response);
+
+ assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
+ assertEquals("admin", username);
+ assertTrue(authenticator.isActive());
+ }
+
+ @Test
+ public void shouldRefreshExpiredTokenAndCallRemoteMapper()
+ {
+ when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
+ when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
+ when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
+ String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
+ when(accessToken.getTokenValue()).thenReturn("REFRESHED_JWT_TOKEN");
+ when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
+ when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
+ when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
+ when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization);
+ when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
+
+ String username = authenticator.getUserId(request, response);
+
+ verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response);
+ verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
+ assertEquals("Bearer REFRESHED_JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
+ assertEquals("admin", username);
+ }
+
+ @Test
+ public void shouldCallAuthChallengeWebScriptHome() throws IOException
+ {
+
+ String redirectPath = "/alfresco/s/index";
+ when(request.getRequestURL()).thenReturn(webScriptHomeURL);
+ when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
+ when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath);
+ ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class);
+ String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
+ .formatted("http://localhost:8080", redirectPath);
+
+ authenticator.requestAuthentication(request, response);
+
+ verify(response).sendRedirect(authenticationRequest.capture());
+ assertTrue(authenticationRequest.getValue().contains(expectedUri));
+ assertTrue(authenticationRequest.getValue().contains("openid"));
+ assertTrue(authenticationRequest.getValue().contains("profile"));
+ assertTrue(authenticationRequest.getValue().contains("email"));
+ assertTrue(authenticationRequest.getValue().contains("offline_access"));
+ assertTrue(authenticationRequest.getValue().contains("state"));
+ }
+
+ @Test
+ public void shouldCallAuthChallengeWebScriptHomeWithAudience() throws IOException
+ {
+ String audience = "http://localhost:8082";
+ String redirectPath = "/alfresco/s/index";
+ when(request.getRequestURL()).thenReturn(webScriptHomeURL);
+ when(identityServiceConfig.getAudience()).thenReturn(audience);
+ when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath);
+ when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
+ ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class);
+ String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
+ .formatted("http://localhost:8080", redirectPath);
+
+ authenticator.requestAuthentication(request, response);
+
+ verify(response).sendRedirect(authenticationRequest.capture());
+ assertTrue(authenticationRequest.getValue().contains(expectedUri));
+ assertTrue(authenticationRequest.getValue().contains("openid"));
+ assertTrue(authenticationRequest.getValue().contains("profile"));
+ assertTrue(authenticationRequest.getValue().contains("email"));
+ assertTrue(authenticationRequest.getValue().contains("offline_access"));
+ assertTrue(authenticationRequest.getValue().contains("audience=%s".formatted(audience)));
+ assertTrue(authenticationRequest.getValue().contains("state"));
+ }
+
+ @Test
+ public void shouldResetCookiesAndCallAuthChallenge() throws IOException
+ {
+ when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
+ when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
+ when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
+ String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
+
+ when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class);
+
+ String username = authenticator.getUserId(request, response);
+
+ verify(cookiesService).resetCookie(ALFRESCO_ACCESS_TOKEN, response);
+ verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response);
+ verify(cookiesService).resetCookie(ALFRESCO_TOKEN_EXPIRATION, response);
+ assertNull(username);
+ }
+
+ @Test
+ public void shouldAuthorizeCodeAndSetCookies()
+ {
+ when(request.getParameter("code")).thenReturn("auth_code");
+ when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
+ when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
+ when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
+ when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
+ when(identityServiceFacade.authorize(
+ AuthorizationGrant.authorizationCode("auth_code", webScriptHomeURL.toString())))
+ .thenReturn(accessTokenAuthorization);
+ when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
+
+ String username = authenticator.getUserId(request, response);
+
+ verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "JWT_TOKEN", response);
+ verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
+ assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
+ assertEquals("admin", username);
+ }
+
+ @Test
+ public void shouldExtractUsernameFromAuthorizationHeader()
+ {
+ when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin");
+
+ String username = authenticator.getUserId(request, response);
+
+ assertEquals("admin", username);
+ }
+}