diff --git a/.secrets.baseline b/.secrets.baseline index a0bd0a92bf..9727d8b8b8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1273,7 +1273,7 @@ "filename": "repository/src/main/resources/alfresco/repository.properties", "hashed_secret": "84551ae5442affc9f1a2d3b4c86ae8b24860149d", "is_verified": false, - "line_number": 770, + "line_number": 771, "is_secret": false } ], @@ -1868,5 +1868,5 @@ } ] }, - "generated_at": "2025-03-27T23:45:41Z" + "generated_at": "2025-04-22T06:32:47Z" } diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java index 8efe42042e..3b756b6c57 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/servlet/RemoteUserAuthenticatorFactory.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2023 Alfresco Software Limited + * Copyright (C) 2005 - 2025 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -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 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 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 families = new HashSet(webScript.getDescription().getFamilys()); + Set 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); diff --git a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml index 5fdf6664b1..8354bd260c 100644 --- a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml +++ b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/calendar/feed.get.desc.xml @@ -5,4 +5,4 @@ guest required internal - \ No newline at end of file + diff --git a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml index b1630853ec..1f5b354797 100644 --- a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml +++ b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml @@ -214,9 +214,13 @@ + ${authentication.alwaysAllowBasicAuthForAdminConsole.enabled} + + ${authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled} + ${authentication.getRemoteUserTimeoutMilliseconds} diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java index 605e4f4c92..d28d1f0ebd 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultAdminConsoleAuthenticator.java @@ -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; } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java new file mode 100644 index 0000000000..aa0b1bf858 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/DefaultWebScriptsHomeAuthenticator.java @@ -0,0 +1,55 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.external; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.alfresco.repo.management.subsystems.ActivateableBean; + +/** + * A default {@link ExternalUserAuthenticator} implementation. Returns null to request a basic auth challenge. + */ +public class DefaultWebScriptsHomeAuthenticator implements ExternalUserAuthenticator, ActivateableBean +{ + @Override + public String getUserId(HttpServletRequest request, HttpServletResponse response) + { + return null; + } + + @Override + public void requestAuthentication(HttpServletRequest request, HttpServletResponse response) + { + // No implementation + } + + @Override + public boolean isActive() + { + return false; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java similarity index 71% rename from repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java rename to repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java index cbfface923..8e8515762c 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/external/AdminConsoleAuthenticator.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/external/ExternalUserAuthenticator.java @@ -29,28 +29,17 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** - * An interface for objects capable of extracting an externally authenticated user ID from the HTTP Admin Console webscript request. + * An interface for objects capable of extracting an externally authenticated user ID from the HTTP request. */ -public interface AdminConsoleAuthenticator +public interface ExternalUserAuthenticator { /** - * Gets an externally authenticated user ID from the HTTP Admin Console webscript request. - * - * @param request - * the request - * @param response - * the response + * Gets an externally authenticated user ID from the HTTP request. + * * @return the user ID or null if the user is unauthenticated */ - String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response); + String getUserId(HttpServletRequest request, HttpServletResponse response); - /** - * Requests an authentication. - * - * @param request - * the request - * @param response - * the response - */ + /* Sends redirect to external site to initiate the OIDC authorization code flow. */ void requestAuthentication(HttpServletRequest request, HttpServletResponse response); } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java index 7593e982e9..8d0bef15c5 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceConfig.java @@ -76,6 +76,18 @@ public class IdentityServiceConfig private String lastNameAttribute; private String emailAttribute; private long jwtClockSkewMs; + private String webScriptsHomeRedirectPath; + private String webScriptsHomeScopes; + + public String getWebScriptsHomeRedirectPath() + { + return webScriptsHomeRedirectPath; + } + + public void setWebScriptsHomeRedirectPath(String webScriptsHomeRedirectPath) + { + this.webScriptsHomeRedirectPath = webScriptsHomeRedirectPath; + } /** * @@ -359,6 +371,18 @@ public class IdentityServiceConfig this.adminConsoleScopes = adminConsoleScopes; } + public Set getWebScriptsHomeScopes() + { + return Stream.of(webScriptsHomeScopes.split(",")) + .map(String::trim) + .collect(Collectors.toUnmodifiableSet()); + } + + public void setWebScriptsHomeScopes(String webScriptsHomeScopes) + { + this.webScriptsHomeScopes = webScriptsHomeScopes; + } + public Set getPasswordGrantScopes() { return Stream.of(passwordGrantScopes.split(",")) diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java similarity index 58% rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java index 8907c2f808..616ae6f34e 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticator.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AbstractIdentityServiceAuthenticator.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication; import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode; import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED; @@ -32,7 +32,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; -import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -50,9 +49,8 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; import org.springframework.web.util.UriComponentsBuilder; -import org.alfresco.repo.management.subsystems.ActivateableBean; import org.alfresco.repo.security.authentication.AuthenticationException; -import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator; +import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator; import org.alfresco.repo.security.authentication.external.RemoteUserMapper; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade; @@ -60,27 +58,26 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant; -/** - * An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow. - */ -public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean +public abstract class AbstractIdentityServiceAuthenticator implements ExternalUserAuthenticator { - private static final Logger LOGGER = LoggerFactory.getLogger(IdentityServiceAdminConsoleAuthenticator.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIdentityServiceAuthenticator.class); private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN"; private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN"; private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION"; - private IdentityServiceConfig identityServiceConfig; - private IdentityServiceFacade identityServiceFacade; - private AdminConsoleAuthenticationCookiesService cookiesService; - private RemoteUserMapper remoteUserMapper; - private boolean isEnabled; + protected IdentityServiceConfig identityServiceConfig; + protected IdentityServiceFacade identityServiceFacade; + protected AdminAuthenticationCookiesService cookiesService; + protected RemoteUserMapper remoteUserMapper; + + protected abstract String getConfiguredRedirectPath(); + + protected abstract Set getConfiguredScopes(); @Override - public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response) + public String getUserId(HttpServletRequest request, HttpServletResponse response) { - // Try to extract username from the authorization header String username = remoteUserMapper.getRemoteUser(request); if (username != null) { @@ -107,16 +104,12 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut return null; } - return remoteUserMapper.getRemoteUser(decorateBearerHeader(bearerToken, request)); + HttpServletRequest wrappedRequest = newRequestWrapper(Map.of("Authorization", "Bearer " + bearerToken), request); + return remoteUserMapper.getRemoteUser(wrappedRequest); } @Override public void requestAuthentication(HttpServletRequest request, HttpServletResponse response) - { - respondWithAuthChallenge(request, response); - } - - private void respondWithAuthChallenge(HttpServletRequest request, HttpServletResponse response) { try { @@ -124,7 +117,8 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut { LOGGER.debug("Responding with the authentication challenge"); } - response.sendRedirect(getAuthenticationRequest(request)); + String authenticationRequest = buildAuthRequestUrl(request); + response.sendRedirect(authenticationRequest); } catch (IOException e) { @@ -133,84 +127,34 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut } } - private String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code) + protected String getRedirectUri(String requestURL) { - String bearerToken = null; - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("Retrieving a response using the Authorization Code at the Token Endpoint"); - } - try - { - AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize( - authorizationCode(code, request.getRequestURL().toString())); - addCookies(response, accessTokenAuthorization); - bearerToken = accessTokenAuthorization.getAccessToken().getTokenValue(); - } - catch (AuthorizationException exception) - { - if (LOGGER.isWarnEnabled()) - { - LOGGER.warn( - "Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}", - exception.getMessage()); - } - } - return bearerToken; + return buildRedirectUri(requestURL, getConfiguredRedirectPath()); } - private String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken) - { - String refreshToken = cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request); - String authTokenExpiration = cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request); - try - { - if (isAuthTokenExpired(authTokenExpiration)) - { - bearerToken = refreshAuthToken(refreshToken, response); - } - } - catch (Exception e) - { - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("Error while trying to refresh Auth Token: {}", e.getMessage()); - } - bearerToken = null; - resetCookies(response); - } - return bearerToken; - } - - private void addCookies(HttpServletResponse response, AccessTokenAuthorization accessTokenAuthorization) - { - cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken().getTokenValue(), response); - cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf( - accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response); - cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response); - } - - private String getAuthenticationRequest(HttpServletRequest request) + public String buildAuthRequestUrl(HttpServletRequest request) { ClientRegistration clientRegistration = identityServiceFacade.getClientRegistration(); State state = new State(); - UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri()) + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails() + .getAuthorizationUri()) .queryParam("client_id", clientRegistration.getClientId()) .queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString())) .queryParam("response_type", "code") - .queryParam("scope", String.join("+", getScopes(clientRegistration))) + .queryParam("scope", String.join("+", getConfiguredScopes(clientRegistration))) .queryParam("state", state.toString()); if (StringUtils.isNotBlank(identityServiceConfig.getAudience())) { - authRequestBuilder.queryParam("audience", identityServiceConfig.getAudience()); + builder.queryParam("audience", identityServiceConfig.getAudience()); } - return authRequestBuilder.build().toUriString(); + return builder.build() + .toUriString(); } - private Set getScopes(ClientRegistration clientRegistration) + private Set getConfiguredScopes(ClientRegistration clientRegistration) { return Optional.ofNullable(clientRegistration.getProviderDetails()) .map(ProviderDetails::getConfigurationMetadata) @@ -223,100 +167,149 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut private Set getSupportedScopes(Scope scopes) { + Set configuredScopes = getConfiguredScopes(); return scopes.stream() - .filter(this::hasAdminConsoleScope) .map(Identifier::getValue) + .filter(configuredScopes::contains) .collect(Collectors.toSet()); } - private boolean hasAdminConsoleScope(Scope.Value scope) - { - return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue()); - } - - private String getRedirectUri(String requestURL) + protected String buildRedirectUri(String requestURL, String overridePath) { try { URI originalUri = new URI(requestURL); - URI redirectUri = new URI(originalUri.getScheme(), originalUri.getAuthority(), identityServiceConfig.getAdminConsoleRedirectPath(), originalUri.getQuery(), originalUri.getFragment()); + String path = overridePath != null ? overridePath : originalUri.getPath(); + + URI redirectUri = new URI( + originalUri.getScheme(), + originalUri.getAuthority(), + path, + originalUri.getQuery(), + originalUri.getFragment()); + return redirectUri.toASCIIString(); } catch (URISyntaxException e) { - LOGGER.error("Error while trying to get the redirect URI and respond with the authentication challenge: {}", e.getMessage(), e); + LOGGER.error("Redirect URI construction failed: {}", e.getMessage(), e); throw new AuthenticationException(e.getMessage(), e); } } - private void resetCookies(HttpServletResponse response) + public void challenge(HttpServletRequest request, HttpServletResponse response) + { + try + { + response.sendRedirect(buildAuthRequestUrl(request)); + } + catch (IOException e) + { + throw new AuthenticationException("Auth redirect failed", e); + } + } + + protected String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code) + { + try + { + AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(authorizationCode(code, getRedirectUri(request.getRequestURL() + .toString()))); + addCookies(response, accessTokenAuthorization); + return accessTokenAuthorization.getAccessToken() + .getTokenValue(); + } + catch (AuthorizationException exception) + { + LOGGER.warn("Error while trying to retrieve token using Authorization Code: {}", exception.getMessage()); + return null; + } + } + + protected String refreshTokenIfNeeded(HttpServletRequest request, HttpServletResponse response, String bearerToken) + { + String refreshToken = cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request); + String authTokenExpiration = cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request); + + try + { + if (isAuthTokenExpired(authTokenExpiration)) + { + bearerToken = refreshAuthToken(refreshToken, response); + } + } + catch (Exception e) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Token refresh failed: {}", e.getMessage()); + } + bearerToken = null; + resetCookies(response); + } + + return bearerToken; + } + + private static boolean isAuthTokenExpired(String authTokenExpiration) + { + return authTokenExpiration == null || Instant.now() + .compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0; + } + + private String refreshAuthToken(String refreshToken, HttpServletResponse response) + { + AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.refreshToken(refreshToken)); + if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null) + { + throw new AuthenticationException("Refresh token response is invalid."); + } + addCookies(response, accessTokenAuthorization); + return accessTokenAuthorization.getAccessToken() + .getTokenValue(); + + } + + protected void addCookies(HttpServletResponse response, AccessTokenAuthorization accessTokenAuthorization) + { + cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken() + .getTokenValue(), response); + cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(accessTokenAuthorization.getAccessToken() + .getExpiresAt() + .toEpochMilli()), response); + cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response); + } + + protected void resetCookies(HttpServletResponse response) { cookiesService.resetCookie(ALFRESCO_TOKEN_EXPIRATION, response); cookiesService.resetCookie(ALFRESCO_ACCESS_TOKEN, response); cookiesService.resetCookie(ALFRESCO_REFRESH_TOKEN, response); } - private String refreshAuthToken(String refreshToken, HttpServletResponse response) + protected HttpServletRequest newRequestWrapper(Map headers, HttpServletRequest request) { - AccessTokenAuthorization accessTokenAuthorization = doRefreshAuthToken(refreshToken); - addCookies(response, accessTokenAuthorization); - return accessTokenAuthorization.getAccessToken().getTokenValue(); + return new AdditionalHeadersHttpServletRequestWrapper(headers, request); } - private AccessTokenAuthorization doRefreshAuthToken(String refreshToken) + // Setters + public void setIdentityServiceConfig(IdentityServiceConfig config) { - AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize( - AuthorizationGrant.refreshToken(refreshToken)); - if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null) - { - throw new AuthenticationException("AccessTokenResponse is null or empty"); - } - return accessTokenAuthorization; + this.identityServiceConfig = config; } - private static boolean isAuthTokenExpired(String authTokenExpiration) + public void setIdentityServiceFacade(IdentityServiceFacade facade) { - return Instant.now().compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0; + this.identityServiceFacade = facade; } - private HttpServletRequest decorateBearerHeader(String authToken, HttpServletRequest servletRequest) + public void setCookiesService(AdminAuthenticationCookiesService service) { - Map additionalHeaders = new HashMap<>(); - additionalHeaders.put("Authorization", "Bearer " + authToken); - return new AdminConsoleHttpServletRequestWrapper(additionalHeaders, servletRequest); + this.cookiesService = service; } - public void setIdentityServiceFacade( - IdentityServiceFacade identityServiceFacade) + public void setRemoteUserMapper(RemoteUserMapper mapper) { - this.identityServiceFacade = identityServiceFacade; - } - - public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper) - { - this.remoteUserMapper = remoteUserMapper; - } - - public void setCookiesService( - AdminConsoleAuthenticationCookiesService cookiesService) - { - this.cookiesService = cookiesService; - } - - public void setIdentityServiceConfig( - IdentityServiceConfig identityServiceConfig) - { - this.identityServiceConfig = identityServiceConfig; - } - - @Override - public boolean isActive() - { - return this.isEnabled; - } - - public void setActive(boolean isEnabled) - { - this.isEnabled = isEnabled; + this.remoteUserMapper = mapper; } } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java similarity index 86% rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java index acec9d55c2..e3bb50d5bf 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapper.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapper.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication; import static java.util.Arrays.asList; import static java.util.Collections.enumeration; @@ -37,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 additionalHeaders; private final HttpServletRequest wrappedRequest; - /** - * Constructs a request object wrapping the given request. - * - * @param request - * the request to wrap - * @throws IllegalArgumentException - * if the request is null - */ - public AdminConsoleHttpServletRequestWrapper(Map additionalHeaders, HttpServletRequest request) + public AdditionalHeadersHttpServletRequestWrapper(Map additionalHeaders, HttpServletRequest request) { super(request); PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders); diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java similarity index 95% rename from repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java rename to repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java index f81b35ec8b..0e2a8d69ab 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesService.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesService.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -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; diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java new file mode 100644 index 0000000000..3d06023da6 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticator.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.identityservice.authentication.admin; + +import java.util.Set; + +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator; +import org.alfresco.repo.security.authentication.identityservice.authentication.AbstractIdentityServiceAuthenticator; + +/** + * An {@link ExternalUserAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow. + */ +public class IdentityServiceAdminConsoleAuthenticator extends AbstractIdentityServiceAuthenticator + implements ExternalUserAuthenticator, ActivateableBean +{ + private boolean isEnabled; + + @Override + protected Set getConfiguredScopes() + { + return identityServiceConfig.getAdminConsoleScopes(); + } + + @Override + protected String getConfiguredRedirectPath() + { + return identityServiceConfig.getAdminConsoleRedirectPath(); + } + + @Override + public boolean isActive() + { + return isEnabled; + } + + public void setActive(boolean isEnabled) + { + this.isEnabled = isEnabled; + } +} diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java new file mode 100644 index 0000000000..030fd912ed --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/authentication/webscripts/IdentityServiceWebScriptsHomeAuthenticator.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.identityservice.authentication.webscripts; + +import java.util.Set; + +import org.alfresco.repo.management.subsystems.ActivateableBean; +import org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator; +import org.alfresco.repo.security.authentication.identityservice.authentication.AbstractIdentityServiceAuthenticator; + +/** + * An {@link ExternalUserAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow. + */ +public class IdentityServiceWebScriptsHomeAuthenticator extends AbstractIdentityServiceAuthenticator + implements ExternalUserAuthenticator, ActivateableBean +{ + private boolean isEnabled; + + @Override + protected String getConfiguredRedirectPath() + { + return identityServiceConfig.getWebScriptsHomeRedirectPath(); + } + + @Override + protected Set getConfiguredScopes() + { + return identityServiceConfig.getWebScriptsHomeScopes(); + } + + @Override + public boolean isActive() + { + return this.isEnabled; + } + + public void setActive(boolean isEnabled) + { + this.isEnabled = isEnabled; + } +} diff --git a/repository/src/main/resources/alfresco/authentication-services-context.xml b/repository/src/main/resources/alfresco/authentication-services-context.xml index 7d4f6d9666..4a31cd1d06 100644 --- a/repository/src/main/resources/alfresco/authentication-services-context.xml +++ b/repository/src/main/resources/alfresco/authentication-services-context.xml @@ -135,7 +135,7 @@ - org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator + org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator org.alfresco.repo.management.subsystems.ActivateableBean @@ -144,6 +144,22 @@ + + + + + + + org.alfresco.repo.security.authentication.external.ExternalUserAuthenticator + org.alfresco.repo.management.subsystems.ActivateableBean + + + + webScriptsHomeAuthenticator + + + diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index fe6889a2ee..39aac6d0e5 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -563,6 +563,7 @@ authentication.ticket.validDuration=PT1H authentication.ticket.useSingleTicketPerUser=true authentication.alwaysAllowBasicAuthForAdminConsole.enabled=true +authentication.alwaysAllowBasicAuthForWebScriptsHome.enabled=true authentication.getRemoteUserTimeoutMilliseconds=10000 # FTP access diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml index e25132c527..824c4ac686 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/external/external-authentication-context.xml @@ -104,4 +104,7 @@ - \ No newline at end of file + + + + diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml index 8bc16b3c06..9416715bcd 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml @@ -170,6 +170,9 @@ ${identity-service.admin-console.scopes:openid,profile,email,offline_access} + + ${identity-service.webscripts-home.scopes:openid,profile,email,offline_access} + ${identity-service.password-grant.scopes:openid,profile,email} @@ -179,6 +182,9 @@ ${identity-service.jwt-clock-skew-ms:0} + + ${identity-service.webscripts-home.redirect-path} + @@ -197,12 +203,12 @@ - - - - + + + + - + ${identity-service.authentication.enabled} @@ -210,7 +216,7 @@ - + @@ -220,6 +226,24 @@ + + + ${identity-service.authentication.enabled} + + + + + + + + + + + + + + + diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties index e6d517c1ad..d130e4e89d 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication.properties @@ -12,11 +12,13 @@ identity-service.resource=alfresco identity-service.credentials.secret= identity-service.public-client=true identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary +identity-service.webscripts-home.redirect-path=/alfresco/s/index identity-service.signature-algorithms=RS256,PS256 identity-service.first-name-attribute=given_name identity-service.last-name-attribute=family_name identity-service.email-attribute=email identity-service.admin-console.scopes=openid,profile,email,offline_access +identity-service.webscripts-home.scopes=openid,profile,email,offline_access identity-service.password-grant.scopes=openid,profile,email identity-service.issuer-attribute=issuer identity-service.jwt-clock-skew-ms=0 diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 29689e1cad..3c594e091b 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -34,11 +34,12 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest; import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest; import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest; -import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest; -import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest; -import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapperUnitTest; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesServiceUnitTest; +import org.alfresco.repo.security.authentication.identityservice.authentication.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest; import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest; import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest; +import org.alfresco.repo.security.authentication.identityservice.webscript.IdentityServiceWebScriptsHomeAuthenticatorUnitTest; import org.alfresco.util.testing.category.DBTests; import org.alfresco.util.testing.category.NonBuildTests; @@ -153,9 +154,10 @@ import org.alfresco.util.testing.category.NonBuildTests; IdentityServiceJITProvisioningHandlerUnitTest.class, AccessTokenToDecodedTokenUserMapperUnitTest.class, TokenUserToOIDCUserMapperUnitTest.class, - AdminConsoleAuthenticationCookiesServiceUnitTest.class, - AdminConsoleHttpServletRequestWrapperUnitTest.class, + AdminAuthenticationCookiesServiceUnitTest.class, + AdditionalHeadersHttpServletRequestWrapperUnitTest.class, IdentityServiceAdminConsoleAuthenticatorUnitTest.class, + IdentityServiceWebScriptsHomeAuthenticatorUnitTest.class, ClientRegistrationProviderUnitTest.class, org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class, org.alfresco.repo.security.authentication.PasswordHashingTest.class, diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java similarity index 91% rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java index 9fcd780c18..751f36d486 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleHttpServletRequestWrapperUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdditionalHeadersHttpServletRequestWrapperUnitTest.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication; import static java.util.Collections.enumeration; import static java.util.Collections.list; @@ -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 DEFAULT_HEADERS = new HashMap() { + private static final Map DEFAULT_HEADERS = new HashMap<>() { { put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE); } }; - private static final Map ADDITIONAL_HEADERS = new HashMap() { + private static final Map 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 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 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 overrideHeaders = new HashMap<>(); overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue); - requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request); + requestWrapper = new AdditionalHeadersHttpServletRequestWrapper(overrideHeaders, request); Enumeration headers = requestWrapper.getHeaders(DEFAULT_HEADER); assertNotNull("The headers enumeration should not be null", headers); assertTrue("The headers enumeration should not be empty", headers.hasMoreElements()); diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java similarity index 94% rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java index 896e3a8a1d..8016801d2d 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/AdminConsoleAuthenticationCookiesServiceUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/AdminAuthenticationCookiesServiceUnitTest.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -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 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); diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java similarity index 94% rename from repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java rename to repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java index 6d1ca62de4..f0f5b4fc15 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/authentication/admin/IdentityServiceAdminConsoleAuthenticatorUnitTest.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.repo.security.authentication.identityservice.admin; +package org.alfresco.repo.security.authentication.identityservice.authentication.admin; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -58,11 +58,12 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException; import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapper; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService; @SuppressWarnings("PMD.AvoidStringBufferField") public class IdentityServiceAdminConsoleAuthenticatorUnitTest { - private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN"; private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN"; private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION"; @@ -76,7 +77,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest @Mock IdentityServiceConfig identityServiceConfig; @Mock - AdminConsoleAuthenticationCookiesService cookiesService; + AdminAuthenticationCookiesService cookiesService; @Mock RemoteUserMapper remoteUserMapper; @Mock @@ -84,7 +85,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest @Mock AccessToken accessToken; @Captor - ArgumentCaptor requestCaptor; + ArgumentCaptor requestCaptor; IdentityServiceAdminConsoleAuthenticator authenticator; @@ -122,7 +123,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest String.valueOf(Instant.now().plusSeconds(60).toEpochMilli())); when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); - String username = authenticator.getAdminConsoleUser(request, response); + String username = authenticator.getUserId(request, response); assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization")); assertEquals("admin", username); @@ -143,7 +144,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization); when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); - String username = authenticator.getAdminConsoleUser(request, response); + String username = authenticator.getUserId(request, response); verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response); verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response); @@ -207,7 +208,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class); - String username = authenticator.getAdminConsoleUser(request, response); + String username = authenticator.getUserId(request, response); verify(cookiesService).resetCookie(ALFRESCO_ACCESS_TOKEN, response); verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response); @@ -228,7 +229,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest .thenReturn(accessTokenAuthorization); when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); - String username = authenticator.getAdminConsoleUser(request, response); + String username = authenticator.getUserId(request, response); verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "JWT_TOKEN", response); verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response); @@ -241,7 +242,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest { when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin"); - String username = authenticator.getAdminConsoleUser(request, response); + String username = authenticator.getUserId(request, response); assertEquals("admin", username); } diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java new file mode 100644 index 0000000000..ee68e528af --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/webscript/IdentityServiceWebScriptsHomeAuthenticatorUnitTest.java @@ -0,0 +1,253 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.security.authentication.identityservice.webscript; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.nimbusds.oauth2.sdk.Scope; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; + +import org.alfresco.repo.security.authentication.external.RemoteUserMapper; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException; +import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdditionalHeadersHttpServletRequestWrapper; +import org.alfresco.repo.security.authentication.identityservice.authentication.AdminAuthenticationCookiesService; +import org.alfresco.repo.security.authentication.identityservice.authentication.webscripts.IdentityServiceWebScriptsHomeAuthenticator; + +@SuppressWarnings("PMD.AvoidStringBufferField") +public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest +{ + + private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN"; + private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN"; + private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION"; + + @Mock + HttpServletRequest request; + @Mock + HttpServletResponse response; + @Mock + IdentityServiceFacade identityServiceFacade; + @Mock + IdentityServiceConfig identityServiceConfig; + @Mock + AdminAuthenticationCookiesService cookiesService; + @Mock + RemoteUserMapper remoteUserMapper; + @Mock + AccessTokenAuthorization accessTokenAuthorization; + @Mock + AccessToken accessToken; + @Captor + ArgumentCaptor requestCaptor; + + IdentityServiceWebScriptsHomeAuthenticator authenticator; + + StringBuffer webScriptHomeURL = new StringBuffer("http://localhost:8080/alfresco/s/index"); + + @Before + public void setup() + { + initMocks(this); + ClientRegistration clientRegistration = mock(ClientRegistration.class); + ProviderDetails providerDetails = mock(ProviderDetails.class); + Scope scope = Scope.parse(Arrays.asList("openid", "profile", "email", "offline_access")); + + when(clientRegistration.getProviderDetails()).thenReturn(providerDetails); + when(clientRegistration.getClientId()).thenReturn("alfresco"); + when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth"); + when(providerDetails.getConfigurationMetadata()).thenReturn(Map.of("scopes_supported", scope)); + when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration); + when(request.getRequestURL()).thenReturn(webScriptHomeURL); + when(remoteUserMapper.getRemoteUser(request)).thenReturn(null); + + authenticator = new IdentityServiceWebScriptsHomeAuthenticator(); + authenticator.setActive(true); + authenticator.setIdentityServiceFacade(identityServiceFacade); + authenticator.setCookiesService(cookiesService); + authenticator.setRemoteUserMapper(remoteUserMapper); + authenticator.setIdentityServiceConfig(identityServiceConfig); + } + + @Test + public void shouldCallRemoteMapperIfTokenIsInCookies() + { + when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("JWT_TOKEN"); + when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn( + String.valueOf(Instant.now().plusSeconds(60).toEpochMilli())); + when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); + + String username = authenticator.getUserId(request, response); + + assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization")); + assertEquals("admin", username); + assertTrue(authenticator.isActive()); + } + + @Test + public void shouldRefreshExpiredTokenAndCallRemoteMapper() + { + when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN"); + when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN"); + when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn( + String.valueOf(Instant.now().minusSeconds(60).toEpochMilli())); + when(accessToken.getTokenValue()).thenReturn("REFRESHED_JWT_TOKEN"); + when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60)); + when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken); + when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN"); + when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenReturn(accessTokenAuthorization); + when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); + + String username = authenticator.getUserId(request, response); + + verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "REFRESHED_JWT_TOKEN", response); + verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response); + assertEquals("Bearer REFRESHED_JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization")); + assertEquals("admin", username); + } + + @Test + public void shouldCallAuthChallengeWebScriptHome() throws IOException + { + + String redirectPath = "/alfresco/s/index"; + when(request.getRequestURL()).thenReturn(webScriptHomeURL); + when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access")); + when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath); + ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class); + String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope=" + .formatted("http://localhost:8080", redirectPath); + + authenticator.requestAuthentication(request, response); + + verify(response).sendRedirect(authenticationRequest.capture()); + assertTrue(authenticationRequest.getValue().contains(expectedUri)); + assertTrue(authenticationRequest.getValue().contains("openid")); + assertTrue(authenticationRequest.getValue().contains("profile")); + assertTrue(authenticationRequest.getValue().contains("email")); + assertTrue(authenticationRequest.getValue().contains("offline_access")); + assertTrue(authenticationRequest.getValue().contains("state")); + } + + @Test + public void shouldCallAuthChallengeWebScriptHomeWithAudience() throws IOException + { + String audience = "http://localhost:8082"; + String redirectPath = "/alfresco/s/index"; + when(request.getRequestURL()).thenReturn(webScriptHomeURL); + when(identityServiceConfig.getAudience()).thenReturn(audience); + when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath); + when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access")); + ArgumentCaptor authenticationRequest = ArgumentCaptor.forClass(String.class); + String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope=" + .formatted("http://localhost:8080", redirectPath); + + authenticator.requestAuthentication(request, response); + + verify(response).sendRedirect(authenticationRequest.capture()); + assertTrue(authenticationRequest.getValue().contains(expectedUri)); + assertTrue(authenticationRequest.getValue().contains("openid")); + assertTrue(authenticationRequest.getValue().contains("profile")); + assertTrue(authenticationRequest.getValue().contains("email")); + assertTrue(authenticationRequest.getValue().contains("offline_access")); + assertTrue(authenticationRequest.getValue().contains("audience=%s".formatted(audience))); + assertTrue(authenticationRequest.getValue().contains("state")); + } + + @Test + public void shouldResetCookiesAndCallAuthChallenge() throws IOException + { + when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN"); + when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN"); + when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn( + String.valueOf(Instant.now().minusSeconds(60).toEpochMilli())); + + when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class); + + String username = authenticator.getUserId(request, response); + + verify(cookiesService).resetCookie(ALFRESCO_ACCESS_TOKEN, response); + verify(cookiesService).resetCookie(ALFRESCO_REFRESH_TOKEN, response); + verify(cookiesService).resetCookie(ALFRESCO_TOKEN_EXPIRATION, response); + assertNull(username); + } + + @Test + public void shouldAuthorizeCodeAndSetCookies() + { + when(request.getParameter("code")).thenReturn("auth_code"); + when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN"); + when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60)); + when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken); + when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN"); + when(identityServiceFacade.authorize( + AuthorizationGrant.authorizationCode("auth_code", webScriptHomeURL.toString()))) + .thenReturn(accessTokenAuthorization); + when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin"); + + String username = authenticator.getUserId(request, response); + + verify(cookiesService).addCookie(ALFRESCO_ACCESS_TOKEN, "JWT_TOKEN", response); + verify(cookiesService).addCookie(ALFRESCO_REFRESH_TOKEN, "REFRESH_TOKEN", response); + assertEquals("Bearer JWT_TOKEN", requestCaptor.getValue().getHeader("Authorization")); + assertEquals("admin", username); + } + + @Test + public void shouldExtractUsernameFromAuthorizationHeader() + { + when(remoteUserMapper.getRemoteUser(request)).thenReturn("admin"); + + String username = authenticator.getUserId(request, response); + + assertEquals("admin", username); + } +}