[MNT-24859] Basic Auth still possible with Keycloak enabled (#3361)

Signed-off-by: cezary-witkowski <cezary.witkowski@hyland.com>
Co-authored-by: Sathish Kumar <ST28@ford.com>
Co-authored-by: pmm <purusothaman.mm@hyland.com>
Co-authored-by: purusothaman-mm <purusothman.mm@hyland.com>
This commit is contained in:
cezary-witkowski
2025-05-27 13:31:00 +02:00
committed by GitHub
parent 3a7157f4a7
commit f77b3b79e5
23 changed files with 830 additions and 240 deletions

View File

@@ -1273,7 +1273,7 @@
"filename": "repository/src/main/resources/alfresco/repository.properties", "filename": "repository/src/main/resources/alfresco/repository.properties",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d", "hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false, "is_verified": false,
"line_number": 770, "line_number": 771,
"is_secret": false "is_secret": false
} }
], ],
@@ -1868,5 +1868,5 @@
} }
] ]
}, },
"generated_at": "2025-03-27T23:45:41Z" "generated_at": "2025-04-22T06:32:47Z"
} }

View File

@@ -2,7 +2,7 @@
* #%L * #%L
* Alfresco Remote API * 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. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
@@ -46,7 +46,7 @@ import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException; import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil; 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.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.web.auth.AuthenticationListener; import org.alfresco.repo.web.auth.AuthenticationListener;
import org.alfresco.repo.web.auth.TicketCredentials; import org.alfresco.repo.web.auth.TicketCredentials;
@@ -71,9 +71,11 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
protected RemoteUserMapper remoteUserMapper; protected RemoteUserMapper remoteUserMapper;
protected AuthenticationComponent authenticationComponent; protected AuthenticationComponent authenticationComponent;
protected AdminConsoleAuthenticator adminConsoleAuthenticator; protected ExternalUserAuthenticator adminConsoleAuthenticator;
protected ExternalUserAuthenticator webScriptsHomeAuthenticator;
private boolean alwaysAllowBasicAuthForAdminConsole = true; private boolean alwaysAllowBasicAuthForAdminConsole = true;
private boolean alwaysAllowBasicAuthForWebScriptsHome = true;
List<String> adminConsoleScriptFamilies; List<String> adminConsoleScriptFamilies;
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT; long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
@@ -97,6 +99,16 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
this.alwaysAllowBasicAuthForAdminConsole = alwaysAllowBasicAuthForAdminConsole; this.alwaysAllowBasicAuthForAdminConsole = alwaysAllowBasicAuthForAdminConsole;
} }
public boolean isAlwaysAllowBasicAuthForWebScriptsHome()
{
return alwaysAllowBasicAuthForWebScriptsHome;
}
public void setAlwaysAllowBasicAuthForWebScriptsHome(boolean alwaysAllowBasicAuthForWebScriptsHome)
{
this.alwaysAllowBasicAuthForWebScriptsHome = alwaysAllowBasicAuthForWebScriptsHome;
}
public List<String> getAdminConsoleScriptFamilies() public List<String> getAdminConsoleScriptFamilies()
{ {
return adminConsoleScriptFamilies; return adminConsoleScriptFamilies;
@@ -118,11 +130,17 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
} }
public void setAdminConsoleAuthenticator( public void setAdminConsoleAuthenticator(
AdminConsoleAuthenticator adminConsoleAuthenticator) ExternalUserAuthenticator adminConsoleAuthenticator)
{ {
this.adminConsoleAuthenticator = adminConsoleAuthenticator; this.adminConsoleAuthenticator = adminConsoleAuthenticator;
} }
public void setWebScriptsHomeAuthenticator(
ExternalUserAuthenticator webScriptsHomeAuthenticator)
{
this.webScriptsHomeAuthenticator = webScriptsHomeAuthenticator;
}
@Override @Override
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res) public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
{ {
@@ -136,6 +154,8 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
*/ */
public class RemoteUserAuthenticator extends BasicHttpAuthenticator 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) public RemoteUserAuthenticator(WebScriptServletRequest req, WebScriptServletResponse res, AuthenticationListener listener)
{ {
super(req, res, listener); super(req, res, listener);
@@ -156,24 +176,47 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
{ {
if (servletReq.getServiceMatch() != null && if (servletReq.getServiceMatch() != null &&
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive()) isAdminConsole(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
{ {
userId = getAdminConsoleUser(); userId = getAdminConsoleUser();
} }
else if (servletReq.getServiceMatch() != null &&
isWebScriptsHome(servletReq.getServiceMatch().getWebScript()) && isWebScriptsHomeAuthenticatorActive())
{
userId = getWebScriptsHomeUser();
}
if (userId == null) if (userId == null)
{ {
if (isAlwaysAllowBasicAuthForAdminConsole()) 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 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) catch (AuthenticationTimeoutException e)
{ {
@@ -252,38 +295,63 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
authenticated = super.authenticate(required, isGuest); authenticated = super.authenticate(required, isGuest);
} }
} }
if (!authenticated && servletReq.getServiceMatch() != null && if (!authenticated && servletReq.getServiceMatch() != null)
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
{ {
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; 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 // return REST call, after a timeout/basic auth challenge
if (LOGGER.isTraceEnabled()) 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, // 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; // 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, // 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); return super.authenticate(required, isGuest);
} }
private boolean shouldUseTimeoutForAdminAccessingAdminConsole(RequiredAuthentication required, boolean isGuest) private boolean shouldUseTimeoutForAdminAccessingAdminConsole(RequiredAuthentication required, boolean isGuest)
{ {
boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest && boolean adminConsoleTimeout = RequiredAuthentication.admin.equals(required) && !isGuest &&
servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()); servletReq.getServiceMatch() != null && isAdminConsole(servletReq.getServiceMatch().getWebScript());
if (LOGGER.isTraceEnabled()) 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() private boolean isRemoteUserMapperActive()
@@ -296,7 +364,12 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return adminConsoleAuthenticator != null && (!(adminConsoleAuthenticator instanceof ActivateableBean) || ((ActivateableBean) adminConsoleAuthenticator).isActive()); 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 if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null
|| webScript.getDescription().getFamilys() == null) || webScript.getDescription().getFamilys() == null)
@@ -310,7 +383,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
} }
// intersect the "family" sets defined // intersect the "family" sets defined
Set<String> families = new HashSet<String>(webScript.getDescription().getFamilys()); Set<String> families = new HashSet<>(webScript.getDescription().getFamilys());
families.retainAll(adminConsoleScriptFamilies); families.retainAll(adminConsoleScriptFamilies);
final boolean isAdminConsole = !families.isEmpty(); final boolean isAdminConsole = !families.isEmpty();
@@ -322,6 +395,23 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
return isAdminConsole; 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 protected String getRemoteUserWithTimeout(boolean useTimeout) throws AuthenticationTimeoutException
{ {
if (!useTimeout) if (!useTimeout)
@@ -417,7 +507,21 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
if (isRemoteUserMapperActive()) 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); logRemoteUserID(userId);

View File

@@ -214,9 +214,13 @@
<property name="authenticationListener" ref="webScriptAuthenticationListener"/> <property name="authenticationListener" ref="webScriptAuthenticationListener"/>
<property name="remoteUserMapper" ref="RemoteUserMapper" /> <property name="remoteUserMapper" ref="RemoteUserMapper" />
<property name="adminConsoleAuthenticator" ref="AdminConsoleAuthenticator" /> <property name="adminConsoleAuthenticator" ref="AdminConsoleAuthenticator" />
<property name="webScriptsHomeAuthenticator" ref="WebScriptsHomeAuthenticator" />
<property name="alwaysAllowBasicAuthForAdminConsole"> <property name="alwaysAllowBasicAuthForAdminConsole">
<value>${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}</value> <value>${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}</value>
</property> </property>
<property name="alwaysAllowBasicAuthForWebScriptsHome">
<value>${authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled}</value>
</property>
<property name="getRemoteUserTimeoutMilliseconds"> <property name="getRemoteUserTimeoutMilliseconds">
<value>${authentication.getRemoteUserTimeoutMilliseconds}</value> <value>${authentication.getRemoteUserTimeoutMilliseconds}</value>
</property> </property>

View File

@@ -31,12 +31,12 @@ import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean; 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 @Override
public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response) public String getUserId(HttpServletRequest request, HttpServletResponse response)
{ {
return null; return null;
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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;
}
}

View File

@@ -29,28 +29,17 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; 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 <code>null</code> if the user is unauthenticated * @return the user ID or <code>null</code> if the user is unauthenticated
*/ */
String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response); String getUserId(HttpServletRequest request, HttpServletResponse response);
/** /* Sends redirect to external site to initiate the OIDC authorization code flow. */
* Requests an authentication.
*
* @param request
* the request
* @param response
* the response
*/
void requestAuthentication(HttpServletRequest request, HttpServletResponse response); void requestAuthentication(HttpServletRequest request, HttpServletResponse response);
} }

View File

@@ -76,6 +76,18 @@ public class IdentityServiceConfig
private String lastNameAttribute; private String lastNameAttribute;
private String emailAttribute; private String emailAttribute;
private long jwtClockSkewMs; 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; this.adminConsoleScopes = adminConsoleScopes;
} }
public Set<String> getWebScriptsHomeScopes()
{
return Stream.of(webScriptsHomeScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setWebScriptsHomeScopes(String webScriptsHomeScopes)
{
this.webScriptsHomeScopes = webScriptsHomeScopes;
}
public Set<String> getPasswordGrantScopes() public Set<String> getPasswordGrantScopes()
{ {
return Stream.of(passwordGrantScopes.split(",")) return Stream.of(passwordGrantScopes.split(","))

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED; 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.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; 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.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.web.util.UriComponentsBuilder; 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.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.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade; 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.AuthorizationException;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
/** public abstract class AbstractIdentityServiceAuthenticator implements ExternalUserAuthenticator
* 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
{ {
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_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN"; private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION"; private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
private IdentityServiceConfig identityServiceConfig; protected IdentityServiceConfig identityServiceConfig;
private IdentityServiceFacade identityServiceFacade; protected IdentityServiceFacade identityServiceFacade;
private AdminConsoleAuthenticationCookiesService cookiesService; protected AdminAuthenticationCookiesService cookiesService;
private RemoteUserMapper remoteUserMapper; protected RemoteUserMapper remoteUserMapper;
private boolean isEnabled;
protected abstract String getConfiguredRedirectPath();
protected abstract Set<String> getConfiguredScopes();
@Override @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); String username = remoteUserMapper.getRemoteUser(request);
if (username != null) if (username != null)
{ {
@@ -107,16 +104,12 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
return null; return null;
} }
return remoteUserMapper.getRemoteUser(decorateBearerHeader(bearerToken, request)); HttpServletRequest wrappedRequest = newRequestWrapper(Map.of("Authorization", "Bearer " + bearerToken), request);
return remoteUserMapper.getRemoteUser(wrappedRequest);
} }
@Override @Override
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response) public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
{
respondWithAuthChallenge(request, response);
}
private void respondWithAuthChallenge(HttpServletRequest request, HttpServletResponse response)
{ {
try try
{ {
@@ -124,7 +117,8 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
{ {
LOGGER.debug("Responding with the authentication challenge"); LOGGER.debug("Responding with the authentication challenge");
} }
response.sendRedirect(getAuthenticationRequest(request)); String authenticationRequest = buildAuthRequestUrl(request);
response.sendRedirect(authenticationRequest);
} }
catch (IOException e) 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; return buildRedirectUri(requestURL, getConfiguredRedirectPath());
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;
} }
private String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken) public String buildAuthRequestUrl(HttpServletRequest request)
{
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)
{ {
ClientRegistration clientRegistration = identityServiceFacade.getClientRegistration(); ClientRegistration clientRegistration = identityServiceFacade.getClientRegistration();
State state = new State(); State state = new State();
UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri()) UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails()
.getAuthorizationUri())
.queryParam("client_id", clientRegistration.getClientId()) .queryParam("client_id", clientRegistration.getClientId())
.queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString())) .queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
.queryParam("response_type", "code") .queryParam("response_type", "code")
.queryParam("scope", String.join("+", getScopes(clientRegistration))) .queryParam("scope", String.join("+", getConfiguredScopes(clientRegistration)))
.queryParam("state", state.toString()); .queryParam("state", state.toString());
if (StringUtils.isNotBlank(identityServiceConfig.getAudience())) 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<String> getScopes(ClientRegistration clientRegistration) private Set<String> getConfiguredScopes(ClientRegistration clientRegistration)
{ {
return Optional.ofNullable(clientRegistration.getProviderDetails()) return Optional.ofNullable(clientRegistration.getProviderDetails())
.map(ProviderDetails::getConfigurationMetadata) .map(ProviderDetails::getConfigurationMetadata)
@@ -223,100 +167,149 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set<String> getSupportedScopes(Scope scopes) private Set<String> getSupportedScopes(Scope scopes)
{ {
Set<String> configuredScopes = getConfiguredScopes();
return scopes.stream() return scopes.stream()
.filter(this::hasAdminConsoleScope)
.map(Identifier::getValue) .map(Identifier::getValue)
.filter(configuredScopes::contains)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
private boolean hasAdminConsoleScope(Scope.Value scope) protected String buildRedirectUri(String requestURL, String overridePath)
{
return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
}
private String getRedirectUri(String requestURL)
{ {
try try
{ {
URI originalUri = new URI(requestURL); 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(); return redirectUri.toASCIIString();
} }
catch (URISyntaxException e) 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); 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_TOKEN_EXPIRATION, response);
cookiesService.resetCookie(ALFRESCO_ACCESS_TOKEN, response); cookiesService.resetCookie(ALFRESCO_ACCESS_TOKEN, response);
cookiesService.resetCookie(ALFRESCO_REFRESH_TOKEN, response); cookiesService.resetCookie(ALFRESCO_REFRESH_TOKEN, response);
} }
private String refreshAuthToken(String refreshToken, HttpServletResponse response) protected HttpServletRequest newRequestWrapper(Map<String, String> headers, HttpServletRequest request)
{ {
AccessTokenAuthorization accessTokenAuthorization = doRefreshAuthToken(refreshToken); return new AdditionalHeadersHttpServletRequestWrapper(headers, request);
addCookies(response, accessTokenAuthorization);
return accessTokenAuthorization.getAccessToken().getTokenValue();
} }
private AccessTokenAuthorization doRefreshAuthToken(String refreshToken) // Setters
public void setIdentityServiceConfig(IdentityServiceConfig config)
{ {
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize( this.identityServiceConfig = config;
AuthorizationGrant.refreshToken(refreshToken));
if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
{
throw new AuthenticationException("AccessTokenResponse is null or empty");
}
return accessTokenAuthorization;
} }
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<String, String> additionalHeaders = new HashMap<>(); this.cookiesService = service;
additionalHeaders.put("Authorization", "Bearer " + authToken);
return new AdminConsoleHttpServletRequestWrapper(additionalHeaders, servletRequest);
} }
public void setIdentityServiceFacade( public void setRemoteUserMapper(RemoteUserMapper mapper)
IdentityServiceFacade identityServiceFacade)
{ {
this.identityServiceFacade = identityServiceFacade; this.remoteUserMapper = mapper;
}
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;
} }
} }

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.Arrays.asList;
import static java.util.Collections.enumeration; import static java.util.Collections.enumeration;
@@ -37,20 +37,12 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyCheck;
public class AdminConsoleHttpServletRequestWrapper extends HttpServletRequestWrapper public class AdditionalHeadersHttpServletRequestWrapper extends HttpServletRequestWrapper
{ {
private final Map<String, String> additionalHeaders; private final Map<String, String> additionalHeaders;
private final HttpServletRequest wrappedRequest; private final HttpServletRequest wrappedRequest;
/** public AdditionalHeadersHttpServletRequestWrapper(Map<String, String> additionalHeaders, HttpServletRequest request)
* Constructs a request object wrapping the given request.
*
* @param request
* the request to wrap
* @throws IllegalArgumentException
* if the request is null
*/
public AdminConsoleHttpServletRequestWrapper(Map<String, String> additionalHeaders, HttpServletRequest request)
{ {
super(request); super(request);
PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders); PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders);

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -34,12 +34,12 @@ import org.alfresco.repo.admin.SysAdminParams;
/** /**
* Service to handle Admin Console authentication-related cookies. * Service to handle Admin Console authentication-related cookies.
*/ */
public class AdminConsoleAuthenticationCookiesService public class AdminAuthenticationCookiesService
{ {
private final SysAdminParams sysAdminParams; private final SysAdminParams sysAdminParams;
private final int cookieLifetime; private final int cookieLifetime;
public AdminConsoleAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime) public AdminAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime)
{ {
this.sysAdminParams = sysAdminParams; this.sysAdminParams = sysAdminParams;
this.cookieLifetime = cookieLifetime; this.cookieLifetime = cookieLifetime;

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> 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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> getConfiguredScopes()
{
return identityServiceConfig.getWebScriptsHomeScopes();
}
@Override
public boolean isActive()
{
return this.isEnabled;
}
public void setActive(boolean isEnabled)
{
this.isEnabled = isEnabled;
}
}

View File

@@ -135,7 +135,7 @@
</property> </property>
<property name="interfaces"> <property name="interfaces">
<list> <list>
<value>org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator</value> <value>org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator</value>
<value>org.alfresco.repo.management.subsystems.ActivateableBean</value> <value>org.alfresco.repo.management.subsystems.ActivateableBean</value>
</list> </list>
</property> </property>
@@ -144,6 +144,22 @@
</property> </property>
</bean> </bean>
<bean id="WebScriptsHomeAuthenticator"
class="org.alfresco.repo.management.subsystems.ChainingSubsystemProxyFactory">
<property name="applicationContextManager">
<ref bean="Authentication" />
</property>
<property name="interfaces">
<list>
<value>org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator</value>
<value>org.alfresco.repo.management.subsystems.ActivateableBean</value>
</list>
</property>
<property name="sourceBeanName">
<value>webScriptsHomeAuthenticator</value>
</property>
</bean>
<!-- Passwords are encoded using MD4 --> <!-- Passwords are encoded using MD4 -->
<!-- This is not ideal and only done to be compatible with NTLM --> <!-- This is not ideal and only done to be compatible with NTLM -->
<!-- authentication against the default authentication mechanism. --> <!-- authentication against the default authentication mechanism. -->

View File

@@ -563,6 +563,7 @@ authentication.ticket.validDuration=PT1H
authentication.ticket.useSingleTicketPerUser=true authentication.ticket.useSingleTicketPerUser=true
authentication.alwaysAllowBasicAuthForAdminConsole.enabled=true authentication.alwaysAllowBasicAuthForAdminConsole.enabled=true
authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled=true
authentication.getRemoteUserTimeoutMilliseconds=10000 authentication.getRemoteUserTimeoutMilliseconds=10000
# FTP access # FTP access

View File

@@ -104,4 +104,7 @@
<ref bean="transactionService" /> <ref bean="transactionService" />
</property> </property>
</bean> </bean>
<bean id="webScriptsHomeAuthenticator" class="org.alfresco.repo.security.authentication.external.DefaultWebScriptsHomeAuthenticator" />
</beans> </beans>

View File

@@ -170,6 +170,9 @@
<property name="adminConsoleScopes"> <property name="adminConsoleScopes">
<value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value> <value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value>
</property> </property>
<property name="webScriptsHomeScopes">
<value>${identity-service.webscripts-home.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="passwordGrantScopes"> <property name="passwordGrantScopes">
<value>${identity-service.password-grant.scopes:openid,profile,email}</value> <value>${identity-service.password-grant.scopes:openid,profile,email}</value>
</property> </property>
@@ -179,6 +182,9 @@
<property name="jwtClockSkewMs"> <property name="jwtClockSkewMs">
<value>${identity-service.jwt-clock-skew-ms:0}</value> <value>${identity-service.jwt-clock-skew-ms:0}</value>
</property> </property>
<property name="webScriptsHomeRedirectPath">
<value>${identity-service.webscripts-home.redirect-path}</value>
</property>
</bean> </bean>
<!-- Enable control over mapping between request and user ID --> <!-- Enable control over mapping between request and user ID -->
@@ -197,12 +203,12 @@
</property> </property>
</bean> </bean>
<bean id="adminConsoleAuthenticationCookiesService" class="org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesService"> <bean id="adminAuthenticationCookiesService" class="org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService">
<constructor-arg ref="sysAdminParams" /> <constructor-arg ref="sysAdminParams" />
<constructor-arg value="${admin.console.cookie.lifetime:86400}" /> <constructor-arg value="${admin.console.cookie.lifetime:86400}" />
</bean> </bean>
<bean id="adminConsoleAuthenticator" class="org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticator"> <bean id="adminConsoleAuthenticator" class="org.alfresco.repo.security.authentication.identityservice.authentication.admin.IdentityServiceAdminConsoleAuthenticator">
<property name="active"> <property name="active">
<value>${identity-service.authentication.enabled}</value> <value>${identity-service.authentication.enabled}</value>
</property> </property>
@@ -210,7 +216,7 @@
<ref bean="identityServiceFacade"/> <ref bean="identityServiceFacade"/>
</property> </property>
<property name="cookiesService"> <property name="cookiesService">
<ref bean="adminConsoleAuthenticationCookiesService" /> <ref bean="adminAuthenticationCookiesService" />
</property> </property>
<property name="remoteUserMapper"> <property name="remoteUserMapper">
<ref bean="remoteUserMapper" /> <ref bean="remoteUserMapper" />
@@ -220,6 +226,24 @@
</property> </property>
</bean> </bean>
<bean id="webScriptsHomeAuthenticator" class="org.alfresco.repo.security.authentication.identityservice.authentication.webscripts.IdentityServiceWebScriptsHomeAuthenticator">
<property name="active">
<value>${identity-service.authentication.enabled}</value>
</property>
<property name="identityServiceFacade">
<ref bean="identityServiceFacade"/>
</property>
<property name="cookiesService">
<ref bean="adminAuthenticationCookiesService" />
</property>
<property name="remoteUserMapper">
<ref bean="remoteUserMapper" />
</property>
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>
</bean>
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler"> <bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
<constructor-arg ref="PersonService"/> <constructor-arg ref="PersonService"/>
<constructor-arg ref="identityServiceFacade"/> <constructor-arg ref="identityServiceFacade"/>

View File

@@ -12,11 +12,13 @@ identity-service.resource=alfresco
identity-service.credentials.secret= identity-service.credentials.secret=
identity-service.public-client=true identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary 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.signature-algorithms=RS256,PS256
identity-service.first-name-attribute=given_name identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name identity-service.last-name-attribute=family_name
identity-service.email-attribute=email identity-service.email-attribute=email
identity-service.admin-console.scopes=openid,profile,email,offline_access 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.password-grant.scopes=openid,profile,email
identity-service.issuer-attribute=issuer identity-service.issuer-attribute=issuer
identity-service.jwt-clock-skew-ms=0 identity-service.jwt-clock-skew-ms=0

View File

@@ -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.IdentityServiceJITProvisioningHandlerUnitTest;
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest; import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest; import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest; import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest; import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesServiceUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest; 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.AccessTokenToDecodedTokenUserMapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest; 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.DBTests;
import org.alfresco.util.testing.category.NonBuildTests; import org.alfresco.util.testing.category.NonBuildTests;
@@ -153,9 +154,10 @@ import org.alfresco.util.testing.category.NonBuildTests;
IdentityServiceJITProvisioningHandlerUnitTest.class, IdentityServiceJITProvisioningHandlerUnitTest.class,
AccessTokenToDecodedTokenUserMapperUnitTest.class, AccessTokenToDecodedTokenUserMapperUnitTest.class,
TokenUserToOIDCUserMapperUnitTest.class, TokenUserToOIDCUserMapperUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class, AdminAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class, AdditionalHeadersHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class, IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
IdentityServiceWebScriptsHomeAuthenticatorUnitTest.class,
ClientRegistrationProviderUnitTest.class, ClientRegistrationProviderUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class, org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class, org.alfresco.repo.security.authentication.PasswordHashingTest.class,

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.enumeration;
import static java.util.Collections.list; import static java.util.Collections.list;
@@ -49,19 +49,18 @@ import org.mockito.Mock;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
@SuppressWarnings("PMD.UseDiamondOperator") @SuppressWarnings("PMD.UseDiamondOperator")
public class AdminConsoleHttpServletRequestWrapperUnitTest public class AdditionalHeadersHttpServletRequestWrapperUnitTest
{ {
private static final String DEFAULT_HEADER = "default_header"; private static final String DEFAULT_HEADER = "default_header";
private static final String DEFAULT_HEADER_VALUE = "default_value"; private static final String DEFAULT_HEADER_VALUE = "default_value";
private static final String ADDITIONAL_HEADER = "additional_header"; private static final String ADDITIONAL_HEADER = "additional_header";
private static final String ADDITIONAL_HEADER_VALUE = "additional_value"; private static final String ADDITIONAL_HEADER_VALUE = "additional_value";
private static final Map<String, String> DEFAULT_HEADERS = new HashMap<String, String>() { private static final Map<String, String> DEFAULT_HEADERS = new HashMap<>() {
{ {
put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE); put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE);
} }
}; };
private static final Map<String, String> ADDITIONAL_HEADERS = new HashMap<String, String>() { private static final Map<String, String> ADDITIONAL_HEADERS = new HashMap<>() {
{ {
put(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE); put(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE);
} }
@@ -69,25 +68,25 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
@Mock @Mock
private HttpServletRequest request; private HttpServletRequest request;
private AdminConsoleHttpServletRequestWrapper requestWrapper; private AdditionalHeadersHttpServletRequestWrapper requestWrapper;
@Before @Before
public void setUp() public void setUp()
{ {
initMocks(this); initMocks(this);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(ADDITIONAL_HEADERS, request); requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(ADDITIONAL_HEADERS, request);
} }
@Test(expected = AlfrescoRuntimeException.class) @Test(expected = AlfrescoRuntimeException.class)
public void wrapperShouldNotBeInstancedWithoutAdditionalHeaders() public void wrapperShouldNotBeInstancedWithoutAdditionalHeaders()
{ {
new AdminConsoleHttpServletRequestWrapper(null, request); new AdditionalHeadersHttpServletRequestWrapper(null, request);
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void wrapperShouldNotBeInstancedWithoutRequestsToWrap() public void wrapperShouldNotBeInstancedWithoutRequestsToWrap()
{ {
new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), null); new AdditionalHeadersHttpServletRequestWrapper(new HashMap<>(), null);
} }
@Test @Test
@@ -112,7 +111,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
{ {
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet())); when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
requestWrapper = new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), request); requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(new HashMap<>(), request);
Enumeration<String> headerNames = requestWrapper.getHeaderNames(); Enumeration<String> headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames); assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements()); assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
@@ -164,7 +163,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map<String, String> overrideHeaders = new HashMap<>(); Map<String, String> overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue); overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request); requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request);
String header = requestWrapper.getHeader(DEFAULT_HEADER); String header = requestWrapper.getHeader(DEFAULT_HEADER);
assertEquals("The header should have the overridden value", overrideHeaderValue, header); assertEquals("The header should have the overridden value", overrideHeaderValue, header);
@@ -204,7 +203,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map<String, String> overrideHeaders = new HashMap<>(); Map<String, String> overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue); overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request); requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request);
Enumeration<String> headers = requestWrapper.getHeaders(DEFAULT_HEADER); Enumeration<String> headers = requestWrapper.getHeaders(DEFAULT_HEADER);
assertNotNull("The headers enumeration should not be null", headers); assertNotNull("The headers enumeration should not be null", headers);
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements()); assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -46,7 +46,7 @@ import org.mockito.Mock;
import org.alfresco.repo.admin.SysAdminParams; import org.alfresco.repo.admin.SysAdminParams;
public class AdminConsoleAuthenticationCookiesServiceUnitTest public class AdminAuthenticationCookiesServiceUnitTest
{ {
private static final int DEFAULT_COOKIE_LIFETIME = 86400; private static final int DEFAULT_COOKIE_LIFETIME = 86400;
private static final String COOKIE_NAME = "cookie"; private static final String COOKIE_NAME = "cookie";
@@ -59,13 +59,13 @@ public class AdminConsoleAuthenticationCookiesServiceUnitTest
private SysAdminParams sysAdminParams; private SysAdminParams sysAdminParams;
@Captor @Captor
private ArgumentCaptor<Cookie> cookieCaptor; private ArgumentCaptor<Cookie> cookieCaptor;
private AdminConsoleAuthenticationCookiesService cookiesService; private AdminAuthenticationCookiesService cookiesService;
@Before @Before
public void setUp() public void setUp()
{ {
initMocks(this); initMocks(this);
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME); cookiesService = new AdminAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME);
} }
@Test @Test
@@ -138,7 +138,7 @@ public class AdminConsoleAuthenticationCookiesServiceUnitTest
public void cookieWithCustomMaxAgeShouldBeAddedToTheResponse() public void cookieWithCustomMaxAgeShouldBeAddedToTheResponse()
{ {
int customMaxAge = 60; int customMaxAge = 60;
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, customMaxAge); cookiesService = new AdminAuthenticationCookiesService(sysAdminParams, customMaxAge);
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https"); when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response); cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #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.assertEquals;
import static org.junit.Assert.assertNull; 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.AccessTokenAuthorization;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException; 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.IdentityServiceFacade.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapper;
import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService;
@SuppressWarnings("PMD.AvoidStringBufferField") @SuppressWarnings("PMD.AvoidStringBufferField")
public class IdentityServiceAdminConsoleAuthenticatorUnitTest public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{ {
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN"; 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_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION"; private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
@@ -76,7 +77,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Mock @Mock
IdentityServiceConfig identityServiceConfig; IdentityServiceConfig identityServiceConfig;
@Mock @Mock
AdminConsoleAuthenticationCookiesService cookiesService; AdminAuthenticationCookiesService cookiesService;
@Mock @Mock
RemoteUserMapper remoteUserMapper; RemoteUserMapper remoteUserMapper;
@Mock @Mock
@@ -84,7 +85,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Mock @Mock
AccessToken accessToken; AccessToken accessToken;
@Captor @Captor
ArgumentCaptor<AdminConsoleHttpServletRequestWrapper> requestCaptor; ArgumentCaptor<AdditionalHeadersHttpServletRequestWrapper> requestCaptor;
IdentityServiceAdminConsoleAuthenticator authenticator; IdentityServiceAdminConsoleAuthenticator authenticator;
@@ -122,7 +123,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String.valueOf(Instant.now().plusSeconds(60).toEpochMilli())); String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); 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("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization"));
assertEquals("admin", username); assertEquals("admin", username);
@@ -143,7 +144,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization); when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); 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_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_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); 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_ACCESS_TOKEN, response);
verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response); verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response);
@@ -228,7 +229,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
.thenReturn(accessTokenAuthorization); .thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); 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_ACCESS_TOKEN, "JWT_TOKEN", response);
verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response); verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response);
@@ -241,7 +242,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{ {
when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin"); when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response); String username = authenticator.getUserId(request, response);
assertEquals("admin", username); assertEquals("admin", username);
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<AdditionalHeadersHttpServletRequestWrapper> 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<String> 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<String> 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);
}
}