[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",
"hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d",
"is_verified": false,
"line_number": 770,
"line_number": 771,
"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
* 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
@@ -46,7 +46,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;
@@ -71,9 +71,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<String> adminConsoleScriptFamilies;
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
@@ -97,6 +99,16 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
this.alwaysAllowBasicAuthForAdminConsole = alwaysAllowBasicAuthForAdminConsole;
}
public boolean isAlwaysAllowBasicAuthForWebScriptsHome()
{
return alwaysAllowBasicAuthForWebScriptsHome;
}
public void setAlwaysAllowBasicAuthForWebScriptsHome(boolean alwaysAllowBasicAuthForWebScriptsHome)
{
this.alwaysAllowBasicAuthForWebScriptsHome = alwaysAllowBasicAuthForWebScriptsHome;
}
public List<String> getAdminConsoleScriptFamilies()
{
return adminConsoleScriptFamilies;
@@ -118,11 +130,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)
{
@@ -136,6 +154,8 @@ 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);
@@ -156,24 +176,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)
{
@@ -252,38 +295,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()
@@ -296,7 +364,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)
@@ -310,7 +383,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
}
// 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);
final boolean isAdminConsole = !families.isEmpty();
@@ -322,6 +395,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)
@@ -417,7 +507,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);

View File

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

View File

@@ -31,12 +31,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;
}

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;
/**
* 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
*/
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);
}

View File

@@ -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<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()
{
return Stream.of(passwordGrantScopes.split(","))

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #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<String> 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<String> getScopes(ClientRegistration clientRegistration)
private Set<String> getConfiguredScopes(ClientRegistration clientRegistration)
{
return Optional.ofNullable(clientRegistration.getProviderDetails())
.map(ProviderDetails::getConfigurationMetadata)
@@ -223,100 +167,149 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set<String> getSupportedScopes(Scope scopes)
{
Set<String> 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<String, String> 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<String, String> 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;
}
}

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #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,20 +37,12 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
import org.alfresco.util.PropertyCheck;
public class AdminConsoleHttpServletRequestWrapper extends HttpServletRequestWrapper
public class AdditionalHeadersHttpServletRequestWrapper extends HttpServletRequestWrapper
{
private final Map<String, String> 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<String, String> additionalHeaders, HttpServletRequest request)
public AdditionalHeadersHttpServletRequestWrapper(Map<String, String> additionalHeaders, HttpServletRequest request)
{
super(request);
PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders);

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #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;
@@ -34,12 +34,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;

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 name="interfaces">
<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>
</list>
</property>
@@ -144,6 +144,22 @@
</property>
</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 -->
<!-- This is not ideal and only done to be compatible with NTLM -->
<!-- authentication against the default authentication mechanism. -->

View File

@@ -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

View File

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

View File

@@ -170,6 +170,9 @@
<property name="adminConsoleScopes">
<value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="webScriptsHomeScopes">
<value>${identity-service.webscripts-home.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="passwordGrantScopes">
<value>${identity-service.password-grant.scopes:openid,profile,email}</value>
</property>
@@ -179,6 +182,9 @@
<property name="jwtClockSkewMs">
<value>${identity-service.jwt-clock-skew-ms:0}</value>
</property>
<property name="webScriptsHomeRedirectPath">
<value>${identity-service.webscripts-home.redirect-path}</value>
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->
@@ -197,12 +203,12 @@
</property>
</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 value="${admin.console.cookie.lifetime:86400}" />
</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">
<value>${identity-service.authentication.enabled}</value>
</property>
@@ -210,7 +216,25 @@
<ref bean="identityServiceFacade"/>
</property>
<property name="cookiesService">
<ref bean="adminConsoleAuthenticationCookiesService" />
<ref bean="adminAuthenticationCookiesService" />
</property>
<property name="remoteUserMapper">
<ref bean="remoteUserMapper" />
</property>
<property name="identityServiceConfig">
<ref bean="identityServiceConfig" />
</property>
</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" />

View File

@@ -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

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.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,

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #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;
@@ -49,19 +49,18 @@ import org.mockito.Mock;
import org.alfresco.error.AlfrescoRuntimeException;
@SuppressWarnings("PMD.UseDiamondOperator")
public class AdminConsoleHttpServletRequestWrapperUnitTest
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<String, String> DEFAULT_HEADERS = new HashMap<String, String>() {
private static final Map<String, String> DEFAULT_HEADERS = new HashMap<>() {
{
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);
}
@@ -69,25 +68,25 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
@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
@@ -112,7 +111,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<String> headerNames = requestWrapper.getHeaderNames();
assertNotNull("headerNames should not be null", headerNames);
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
@@ -164,7 +163,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map<String, String> 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);
@@ -204,7 +203,7 @@ public class AdminConsoleHttpServletRequestWrapperUnitTest
Map<String, String> overrideHeaders = new HashMap<>();
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request);
Enumeration<String> headers = requestWrapper.getHeaders(DEFAULT_HEADER);
assertNotNull("The headers enumeration should not be null", headers);
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/>.
* #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;
@@ -46,7 +46,7 @@ import org.mockito.Mock;
import org.alfresco.repo.admin.SysAdminParams;
public class AdminConsoleAuthenticationCookiesServiceUnitTest
public class AdminAuthenticationCookiesServiceUnitTest
{
private static final int DEFAULT_COOKIE_LIFETIME = 86400;
private static final String COOKIE_NAME = "cookie";
@@ -59,13 +59,13 @@ public class AdminConsoleAuthenticationCookiesServiceUnitTest
private SysAdminParams sysAdminParams;
@Captor
private ArgumentCaptor<Cookie> 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
@@ -138,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);

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #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<AdminConsoleHttpServletRequestWrapper> requestCaptor;
ArgumentCaptor<AdditionalHeadersHttpServletRequestWrapper> 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);
}

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);
}
}