mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
MNT-23880 Integrate the Alfresco Admin Console with the IDS (#2362)
* MNT-23880 Integrate IDS with the Admin Console * MNT-23880 Remove diamond expressions * MNT-23880 Refactor * MNT-23880 Move requesting authentication * MNT-23880 Fix comment * MNT-23880 Check if AdminConsoleAuthenticator is active * MNT-23880 Fix DefaultAdminConsoleAuthenticator + PMD issues * MNT-23880 Fix PMD issues * MNT-23880 Refactor * MNT-23880 Refactor RemoteUserAuthenticatorFactory
This commit is contained in:
@@ -34,6 +34,7 @@ import org.alfresco.repo.management.subsystems.ActivateableBean;
|
|||||||
import org.alfresco.repo.security.authentication.AuthenticationComponent;
|
import org.alfresco.repo.security.authentication.AuthenticationComponent;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||||
import org.alfresco.repo.web.auth.AuthenticationListener;
|
import org.alfresco.repo.web.auth.AuthenticationListener;
|
||||||
import org.alfresco.repo.web.auth.TicketCredentials;
|
import org.alfresco.repo.web.auth.TicketCredentials;
|
||||||
@@ -67,16 +68,18 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactory
|
public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactory
|
||||||
{
|
{
|
||||||
private static Log logger = LogFactory.getLog(RemoteUserAuthenticatorFactory.class);
|
private static final Log LOGGER = LogFactory.getLog(RemoteUserAuthenticatorFactory.class);
|
||||||
public static final long GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT = 10000L; // 10 sec
|
public static final long GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT = 10000L; // 10 sec
|
||||||
|
|
||||||
protected RemoteUserMapper remoteUserMapper;
|
protected RemoteUserMapper remoteUserMapper;
|
||||||
protected AuthenticationComponent authenticationComponent;
|
protected AuthenticationComponent authenticationComponent;
|
||||||
|
protected AdminConsoleAuthenticator adminConsoleAuthenticator;
|
||||||
|
|
||||||
private boolean alwaysAllowBasicAuthForAdminConsole = true;
|
private boolean alwaysAllowBasicAuthForAdminConsole = true;
|
||||||
List<String> adminConsoleScriptFamilies;
|
List<String> adminConsoleScriptFamilies;
|
||||||
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
|
long getRemoteUserTimeoutMilliseconds = GET_REMOTE_USER_TIMEOUT_MILLISECONDS_DEFAULT;
|
||||||
|
|
||||||
|
|
||||||
public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
|
public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
|
||||||
{
|
{
|
||||||
this.remoteUserMapper = remoteUserMapper;
|
this.remoteUserMapper = remoteUserMapper;
|
||||||
@@ -117,6 +120,12 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
this.getRemoteUserTimeoutMilliseconds = getRemoteUserTimeoutMilliseconds;
|
this.getRemoteUserTimeoutMilliseconds = getRemoteUserTimeoutMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAdminConsoleAuthenticator(
|
||||||
|
AdminConsoleAuthenticator adminConsoleAuthenticator)
|
||||||
|
{
|
||||||
|
this.adminConsoleAuthenticator = adminConsoleAuthenticator;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
|
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
|
||||||
{
|
{
|
||||||
@@ -140,36 +149,46 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
{
|
{
|
||||||
boolean authenticated = false;
|
boolean authenticated = false;
|
||||||
|
|
||||||
if (logger.isTraceEnabled())
|
if (LOGGER.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("Authenticate level required: " + required + " is guest: " + isGuest);
|
LOGGER.trace("Authenticate level required: " + required + " is guest: " + isGuest);
|
||||||
}
|
}
|
||||||
|
|
||||||
String userId = null;
|
String userId = null;
|
||||||
if (isRemoteUserMapperActive())
|
if (isRemoteUserMapperActive())
|
||||||
{
|
{
|
||||||
if (isAlwaysAllowBasicAuthForAdminConsole())
|
|
||||||
{
|
|
||||||
final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
|
|
||||||
|
|
||||||
if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin())
|
if (servletReq.getServiceMatch() != null &&
|
||||||
{
|
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
|
||||||
return callBasicAuthForAdminConsoleAccess(required, isGuest);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole);
|
|
||||||
}
|
|
||||||
catch (AuthenticationTimeoutException e)
|
|
||||||
{
|
|
||||||
//return basic auth challenge
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// retrieve the remote user if configured and available - authenticate that user directly
|
userId = getAdminConsoleUser();
|
||||||
userId = getRemoteUser();
|
}
|
||||||
|
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
if (isAlwaysAllowBasicAuthForAdminConsole())
|
||||||
|
{
|
||||||
|
final boolean useTimeoutForAdminAccessingAdminConsole = shouldUseTimeoutForAdminAccessingAdminConsole(required, isGuest);
|
||||||
|
|
||||||
|
if (useTimeoutForAdminAccessingAdminConsole && isBasicAuthHeaderPresentForAdmin())
|
||||||
|
{
|
||||||
|
return callBasicAuthForAdminConsoleAccess(required, isGuest);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userId = getRemoteUserWithTimeout(useTimeoutForAdminAccessingAdminConsole);
|
||||||
|
}
|
||||||
|
catch (AuthenticationTimeoutException e)
|
||||||
|
{
|
||||||
|
//return basic auth challenge
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// retrieve the remote user if configured and available - authenticate that user directly
|
||||||
|
userId = getRemoteUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +227,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
{
|
{
|
||||||
// Validate the ticket for the current SessionUser
|
// Validate the ticket for the current SessionUser
|
||||||
authenticationService.validate(user.getTicket());
|
authenticationService.validate(user.getTicket());
|
||||||
if (logger.isDebugEnabled())
|
if (LOGGER.isDebugEnabled())
|
||||||
{
|
{
|
||||||
logger.debug("Ticket is valid. Retaining cached user in session.");
|
LOGGER.debug("Ticket is valid. Retaining cached user in session.");
|
||||||
}
|
}
|
||||||
listener.userAuthenticated(new TicketCredentials(user.getTicket()));
|
listener.userAuthenticated(new TicketCredentials(user.getTicket()));
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
@@ -222,9 +241,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
}
|
}
|
||||||
catch (AuthenticationException authErr)
|
catch (AuthenticationException authErr)
|
||||||
{
|
{
|
||||||
if (logger.isDebugEnabled())
|
if (LOGGER.isDebugEnabled())
|
||||||
{
|
{
|
||||||
logger.debug("An Authentication error occur. Removing User session.", authErr);
|
LOGGER.debug("An Authentication error occur. Removing User session.", authErr);
|
||||||
}
|
}
|
||||||
session.removeAttribute(AuthenticationDriver.AUTHENTICATION_USER);
|
session.removeAttribute(AuthenticationDriver.AUTHENTICATION_USER);
|
||||||
session.invalidate();
|
session.invalidate();
|
||||||
@@ -236,15 +255,20 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
authenticated = super.authenticate(required, isGuest);
|
authenticated = super.authenticate(required, isGuest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!authenticated && servletReq.getServiceMatch() != null &&
|
||||||
|
isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript()) && isAdminConsoleAuthenticatorActive())
|
||||||
|
{
|
||||||
|
adminConsoleAuthenticator.requestAuthentication(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
|
||||||
|
}
|
||||||
return authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean callBasicAuthForAdminConsoleAccess(RequiredAuthentication required, boolean isGuest)
|
private boolean callBasicAuthForAdminConsoleAccess(RequiredAuthentication required, boolean isGuest)
|
||||||
{
|
{
|
||||||
// return REST call, after a timeout/basic auth challenge
|
// return REST call, after a timeout/basic auth challenge
|
||||||
if (logger.isTraceEnabled())
|
if (LOGGER.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("An Admin Console request has come in with Basic Auth headers present for an admin user.");
|
LOGGER.trace("An Admin Console request has come in with Basic Auth headers present for an admin user.");
|
||||||
}
|
}
|
||||||
// In order to prompt for another password, in case it was not entered correctly,
|
// In order to prompt for another password, in case it was not entered correctly,
|
||||||
// the output of this method should be returned by the calling "authenticate" method;
|
// the output of this method should be returned by the calling "authenticate" method;
|
||||||
@@ -258,9 +282,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest &&
|
boolean useTimeoutForAdminAccessingAdminConsole = RequiredAuthentication.admin.equals(required) && !isGuest &&
|
||||||
servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript());
|
servletReq.getServiceMatch() != null && isAdminConsoleWebScript(servletReq.getServiceMatch().getWebScript());
|
||||||
|
|
||||||
if (logger.isTraceEnabled())
|
if (LOGGER.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole);
|
LOGGER.trace("Should ensure that the admins can login with basic auth: " + useTimeoutForAdminAccessingAdminConsole);
|
||||||
}
|
}
|
||||||
return useTimeoutForAdminAccessingAdminConsole;
|
return useTimeoutForAdminAccessingAdminConsole;
|
||||||
}
|
}
|
||||||
@@ -270,6 +294,11 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
return remoteUserMapper != null && (!(remoteUserMapper instanceof ActivateableBean) || ((ActivateableBean) remoteUserMapper).isActive());
|
return remoteUserMapper != null && (!(remoteUserMapper instanceof ActivateableBean) || ((ActivateableBean) remoteUserMapper).isActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isAdminConsoleAuthenticatorActive()
|
||||||
|
{
|
||||||
|
return adminConsoleAuthenticator != null && (!(adminConsoleAuthenticator instanceof ActivateableBean) || ((ActivateableBean) adminConsoleAuthenticator).isActive());
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean isAdminConsoleWebScript(WebScript webScript)
|
protected boolean isAdminConsoleWebScript(WebScript webScript)
|
||||||
{
|
{
|
||||||
if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null
|
if (webScript == null || adminConsoleScriptFamilies == null || webScript.getDescription() == null
|
||||||
@@ -278,9 +307,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logger.isTraceEnabled())
|
if (LOGGER.isTraceEnabled())
|
||||||
{
|
{
|
||||||
logger.trace("WebScript: " + webScript + " has these families: " + webScript.getDescription().getFamilys());
|
LOGGER.trace("WebScript: " + webScript + " has these families: " + webScript.getDescription().getFamilys());
|
||||||
}
|
}
|
||||||
|
|
||||||
// intersect the "family" sets defined
|
// intersect the "family" sets defined
|
||||||
@@ -288,9 +317,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
families.retainAll(adminConsoleScriptFamilies);
|
families.retainAll(adminConsoleScriptFamilies);
|
||||||
final boolean isAdminConsole = !families.isEmpty();
|
final boolean isAdminConsole = !families.isEmpty();
|
||||||
|
|
||||||
if (logger.isTraceEnabled() && isAdminConsole)
|
if (LOGGER.isTraceEnabled() && isAdminConsole)
|
||||||
{
|
{
|
||||||
logger.trace("Detected an Admin Console webscript: " + webScript );
|
LOGGER.trace("Detected an Admin Console webscript: " + webScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAdminConsole;
|
return isAdminConsole;
|
||||||
@@ -316,7 +345,7 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.warn("Exception trying to get the remote user: " + e.getMessage(), e);
|
LOGGER.warn("Exception trying to get the remote user: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
returnedRemoteUser = getRemoteUserRunnable.getReturnedRemoteUser();
|
returnedRemoteUser = getRemoteUserRunnable.getReturnedRemoteUser();
|
||||||
@@ -330,9 +359,9 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
final String message = "Could not get the remote user in a reasonable time: " + getRemoteUserTimeoutMilliseconds + " milliseconds. "
|
final String message = "Could not get the remote user in a reasonable time: " + getRemoteUserTimeoutMilliseconds + " milliseconds. "
|
||||||
+ "Adjust the timeout property 'authentication.getRemoteUserTimeoutMilliseconds' if required.";
|
+ "Adjust the timeout property 'authentication.getRemoteUserTimeoutMilliseconds' if required.";
|
||||||
|
|
||||||
if (logger.isWarnEnabled())
|
if (LOGGER.isWarnEnabled())
|
||||||
{
|
{
|
||||||
logger.warn("Returning basic auth challenge for Admin Console. Cause: " + message);
|
LOGGER.warn("Returning basic auth challenge for Admin Console. Cause: " + message);
|
||||||
}
|
}
|
||||||
HttpServletResponse res = servletRes.getHttpServletResponse();
|
HttpServletResponse res = servletRes.getHttpServletResponse();
|
||||||
res.setStatus(401);
|
res.setStatus(401);
|
||||||
@@ -379,15 +408,29 @@ public class RemoteUserAuthenticatorFactory extends BasicHttpAuthenticatorFactor
|
|||||||
|
|
||||||
private void logRemoteUserID(String userId)
|
private void logRemoteUserID(String userId)
|
||||||
{
|
{
|
||||||
if (logger.isDebugEnabled())
|
if (LOGGER.isDebugEnabled())
|
||||||
{
|
{
|
||||||
String message = (userId == null) ?
|
String message = (userId == null) ?
|
||||||
"No external user ID in request." :
|
"No external user ID in request." :
|
||||||
"Extracted external user ID from request: " + AuthenticationUtil.maskUsername(userId);
|
"Extracted external user ID from request: " + AuthenticationUtil.maskUsername(userId);
|
||||||
logger.debug(message);
|
LOGGER.debug(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getAdminConsoleUser()
|
||||||
|
{
|
||||||
|
String userId = null;
|
||||||
|
|
||||||
|
if (isRemoteUserMapperActive())
|
||||||
|
{
|
||||||
|
userId = adminConsoleAuthenticator.getAdminConsoleUser(this.servletReq.getHttpServletRequest(), this.servletRes.getHttpServletResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
logRemoteUserID(userId);
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
class GetRemoteUserRunnable implements Runnable
|
class GetRemoteUserRunnable implements Runnable
|
||||||
{
|
{
|
||||||
private volatile String returnedRemoteUser;
|
private volatile String returnedRemoteUser;
|
||||||
|
@@ -213,6 +213,7 @@
|
|||||||
<property name="authenticationComponent" ref="authenticationComponent" />
|
<property name="authenticationComponent" ref="authenticationComponent" />
|
||||||
<property name="authenticationListener" ref="webScriptAuthenticationListener"/>
|
<property name="authenticationListener" ref="webScriptAuthenticationListener"/>
|
||||||
<property name="remoteUserMapper" ref="RemoteUserMapper" />
|
<property name="remoteUserMapper" ref="RemoteUserMapper" />
|
||||||
|
<property name="adminConsoleAuthenticator" ref="AdminConsoleAuthenticator" />
|
||||||
<property name="alwaysAllowBasicAuthForAdminConsole">
|
<property name="alwaysAllowBasicAuthForAdminConsole">
|
||||||
<value>${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}</value>
|
<value>${authentication.alwaysAllowBasicAuthForAdminConsole.enabled}</value>
|
||||||
</property>
|
</property>
|
||||||
|
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for objects capable of extracting an externally authenticated user ID from the HTTP Admin Console webscript request.
|
||||||
|
*/
|
||||||
|
public interface AdminConsoleAuthenticator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Gets an externally authenticated user ID from the HTTP Admin Console webscript 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests an authentication.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* the request
|
||||||
|
* @param response
|
||||||
|
* the response
|
||||||
|
*/
|
||||||
|
void requestAuthentication(HttpServletRequest request, HttpServletResponse response);
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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 AdminConsoleAuthenticator} implementation. Returns null to request a basic auth challenge.
|
||||||
|
*/
|
||||||
|
public class DefaultAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
{
|
||||||
|
// No implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -32,6 +32,9 @@ import java.time.Instant;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to interact with the Identity Service
|
* Allows to interact with the Identity Service
|
||||||
*/
|
*/
|
||||||
@@ -61,6 +64,11 @@ public interface IdentityServiceFacade
|
|||||||
*/
|
*/
|
||||||
Optional<OIDCUserInfo> getUserInfo(String token);
|
Optional<OIDCUserInfo> getUserInfo(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a client registration
|
||||||
|
*/
|
||||||
|
ClientRegistration getClientRegistration();
|
||||||
|
|
||||||
class IdentityServiceFacadeException extends RuntimeException
|
class IdentityServiceFacadeException extends RuntimeException
|
||||||
{
|
{
|
||||||
public IdentityServiceFacadeException(String message)
|
public IdentityServiceFacadeException(String message)
|
||||||
@@ -216,8 +224,14 @@ public interface IdentityServiceFacade
|
|||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o)
|
public boolean equals(Object o)
|
||||||
{
|
{
|
||||||
if (this == o) return true;
|
if (this == o)
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
AuthorizationGrant that = (AuthorizationGrant) o;
|
AuthorizationGrant that = (AuthorizationGrant) o;
|
||||||
return Objects.equals(username, that.username) &&
|
return Objects.equals(username, that.username) &&
|
||||||
Objects.equals(password, that.password) &&
|
Objects.equals(password, that.password) &&
|
||||||
|
@@ -194,6 +194,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
return getTargetFacade().getUserInfo(token);
|
return getTargetFacade().getUserInfo(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistration getClientRegistration()
|
||||||
|
{
|
||||||
|
return getTargetFacade().getClientRegistration();
|
||||||
|
}
|
||||||
|
|
||||||
private IdentityServiceFacade getTargetFacade()
|
private IdentityServiceFacade getTargetFacade()
|
||||||
{
|
{
|
||||||
return ofNullable(targetFacade.get())
|
return ofNullable(targetFacade.get())
|
||||||
@@ -347,7 +353,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
private char[] asCharArray(String value, char[] nullValue)
|
private char[] asCharArray(String value, char[] nullValue)
|
||||||
{
|
{
|
||||||
return Optional.ofNullable(value)
|
return ofNullable(value)
|
||||||
.filter(not(String::isBlank))
|
.filter(not(String::isBlank))
|
||||||
.map(String::toCharArray)
|
.map(String::toCharArray)
|
||||||
.orElse(nullValue);
|
.orElse(nullValue);
|
||||||
|
@@ -147,6 +147,12 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
.map(userInfo -> new OIDCUserInfo(userInfo.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress()));
|
.map(userInfo -> new OIDCUserInfo(userInfo.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistration getClientRegistration()
|
||||||
|
{
|
||||||
|
return clientRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DecodedAccessToken decodeToken(String token)
|
public DecodedAccessToken decodeToken(String token)
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.alfresco.repo.admin.SysAdminParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle Admin Console authentication-related cookies.
|
||||||
|
*/
|
||||||
|
public class AdminConsoleAuthenticationCookiesService
|
||||||
|
{
|
||||||
|
private final SysAdminParams sysAdminParams;
|
||||||
|
private final int cookieLifetime;
|
||||||
|
|
||||||
|
public AdminConsoleAuthenticationCookiesService(SysAdminParams sysAdminParams, int cookieLifetime)
|
||||||
|
{
|
||||||
|
this.sysAdminParams = sysAdminParams;
|
||||||
|
this.cookieLifetime = cookieLifetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cookie with the given name.
|
||||||
|
*
|
||||||
|
* @param name the name of the cookie
|
||||||
|
* @param request the request that might contain the cookie
|
||||||
|
* @return the cookie, or null if the cookie cannot be found
|
||||||
|
*/
|
||||||
|
public String getCookie(String name, HttpServletRequest request)
|
||||||
|
{
|
||||||
|
String result = null;
|
||||||
|
Cookie[] cookies = request.getCookies();
|
||||||
|
|
||||||
|
if (cookies != null)
|
||||||
|
{
|
||||||
|
for (Cookie cookie : cookies)
|
||||||
|
{
|
||||||
|
if (cookie.getName().equals(name))
|
||||||
|
{
|
||||||
|
result = cookie.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a cookie to the response.
|
||||||
|
*
|
||||||
|
* @param name the name of the cookie
|
||||||
|
* @param value the value of the cookie
|
||||||
|
* @param servletResponse the response to add the cookie to
|
||||||
|
*/
|
||||||
|
public void addCookie(String name, String value, HttpServletResponse servletResponse)
|
||||||
|
{
|
||||||
|
internalAddCookie(name, value, cookieLifetime, servletResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue a cookie reset within the given response.
|
||||||
|
*
|
||||||
|
* @param name the cookie to reset
|
||||||
|
* @param servletResponse the response to issue the cookie reset
|
||||||
|
*/
|
||||||
|
public void resetCookie(String name, HttpServletResponse servletResponse)
|
||||||
|
{
|
||||||
|
internalAddCookie(name, "", 0, servletResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void internalAddCookie(String name, String value, int maxAge, HttpServletResponse servletResponse)
|
||||||
|
{
|
||||||
|
Cookie authCookie = new Cookie(name, value);
|
||||||
|
authCookie.setPath("/");
|
||||||
|
authCookie.setMaxAge(maxAge);
|
||||||
|
authCookie.setSecure(sysAdminParams.getAlfrescoProtocol().equalsIgnoreCase("https"));
|
||||||
|
authCookie.setHttpOnly(true);
|
||||||
|
servletResponse.addCookie(authCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.enumeration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import org.alfresco.util.PropertyCheck;
|
||||||
|
|
||||||
|
public class AdminConsoleHttpServletRequestWrapper 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)
|
||||||
|
{
|
||||||
|
super(request);
|
||||||
|
PropertyCheck.mandatory(this, "additionalHeaders", additionalHeaders);
|
||||||
|
this.additionalHeaders = additionalHeaders;
|
||||||
|
this.wrappedRequest = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaderNames()
|
||||||
|
{
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
Enumeration<String> originalHeaders = wrappedRequest.getHeaderNames();
|
||||||
|
if (originalHeaders != null)
|
||||||
|
{
|
||||||
|
while (originalHeaders.hasMoreElements())
|
||||||
|
{
|
||||||
|
String header = originalHeaders.nextElement();
|
||||||
|
if (!additionalHeaders.containsKey(header))
|
||||||
|
{
|
||||||
|
result.add(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addAll(additionalHeaders.keySet());
|
||||||
|
return enumeration(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeader(String name)
|
||||||
|
{
|
||||||
|
return additionalHeaders.getOrDefault(name, super.getHeader(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getHeaders(String name)
|
||||||
|
{
|
||||||
|
return enumeration(asList(additionalHeaders.getOrDefault(name, super.getHeader(name))));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant.authorizationCode;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
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.RemoteUserMapper;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
|
||||||
|
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.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID
|
||||||
|
* or to initiate the OIDC authorization code flow.
|
||||||
|
*/
|
||||||
|
public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
|
||||||
|
{
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(IdentityServiceAdminConsoleAuthenticator.class);
|
||||||
|
|
||||||
|
private static final 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 IdentityServiceFacade identityServiceFacade;
|
||||||
|
private AdminConsoleAuthenticationCookiesService cookiesService;
|
||||||
|
private RemoteUserMapper remoteUserMapper;
|
||||||
|
private boolean isEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAdminConsoleUser(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
{
|
||||||
|
// Try to extract username from the authorization header
|
||||||
|
String username = remoteUserMapper.getRemoteUser(request);
|
||||||
|
if (username != null)
|
||||||
|
{
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String bearerToken = cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request);
|
||||||
|
|
||||||
|
if (bearerToken != null)
|
||||||
|
{
|
||||||
|
bearerToken = refreshTokenIfNeeded(request, response, bearerToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
String code = request.getParameter("code");
|
||||||
|
if (code != null)
|
||||||
|
{
|
||||||
|
bearerToken = retrieveTokenUsingAuthCode(request, response, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bearerToken == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteUserMapper.getRemoteUser(decorateBearerHeader(bearerToken, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
{
|
||||||
|
respondWithAuthChallenge(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void respondWithAuthChallenge(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (LOGGER.isDebugEnabled())
|
||||||
|
{
|
||||||
|
LOGGER.debug("Responding with the authentication challenge");
|
||||||
|
}
|
||||||
|
response.sendRedirect(getAuthenticationRequest(request));
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
LOGGER.error("Error while trying to respond with the authentication challenge: {}", e.getMessage(), e);
|
||||||
|
throw new AuthenticationException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String retrieveTokenUsingAuthCode(HttpServletRequest request, HttpServletResponse response, String code)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return identityServiceFacade.getClientRegistration().getProviderDetails().getAuthorizationUri()
|
||||||
|
+ "?client_id="
|
||||||
|
+ identityServiceFacade.getClientRegistration().getClientId()
|
||||||
|
+ "&redirect_uri="
|
||||||
|
+ request.getRequestURL()
|
||||||
|
+ "&response_type=code"
|
||||||
|
+ "&scope=openid";
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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)
|
||||||
|
{
|
||||||
|
AccessTokenAuthorization accessTokenAuthorization = doRefreshAuthToken(refreshToken);
|
||||||
|
addCookies(response, accessTokenAuthorization);
|
||||||
|
return accessTokenAuthorization.getAccessToken().getTokenValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessTokenAuthorization doRefreshAuthToken(String refreshToken)
|
||||||
|
{
|
||||||
|
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
|
||||||
|
AuthorizationGrant.refreshToken(refreshToken));
|
||||||
|
if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("AccessTokenResponse is null or empty");
|
||||||
|
}
|
||||||
|
return accessTokenAuthorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAuthTokenExpired(String authTokenExpiration)
|
||||||
|
{
|
||||||
|
return Instant.now().compareTo(Instant.ofEpochMilli(Long.parseLong(authTokenExpiration))) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpServletRequest decorateBearerHeader(String authToken, HttpServletRequest servletRequest)
|
||||||
|
{
|
||||||
|
Map<String, String> additionalHeaders = new HashMap<>();
|
||||||
|
additionalHeaders.put("Authorization", "Bearer " + authToken);
|
||||||
|
return new AdminConsoleHttpServletRequestWrapper(additionalHeaders, servletRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentityServiceFacade(
|
||||||
|
IdentityServiceFacade identityServiceFacade)
|
||||||
|
{
|
||||||
|
this.identityServiceFacade = identityServiceFacade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemoteUserMapper(RemoteUserMapper remoteUserMapper)
|
||||||
|
{
|
||||||
|
this.remoteUserMapper = remoteUserMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCookiesService(
|
||||||
|
AdminConsoleAuthenticationCookiesService cookiesService)
|
||||||
|
{
|
||||||
|
this.cookiesService = cookiesService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive()
|
||||||
|
{
|
||||||
|
return this.isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActive(boolean isEnabled)
|
||||||
|
{
|
||||||
|
this.isEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
}
|
@@ -128,6 +128,22 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="AdminConsoleAuthenticator"
|
||||||
|
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.AdminConsoleAuthenticator</value>
|
||||||
|
<value>org.alfresco.repo.management.subsystems.ActivateableBean</value>
|
||||||
|
</list>
|
||||||
|
</property>
|
||||||
|
<property name="sourceBeanName">
|
||||||
|
<value>adminConsoleAuthenticator</value>
|
||||||
|
</property>
|
||||||
|
</bean>
|
||||||
|
|
||||||
<!-- Passwords are encoded using MD4 -->
|
<!-- Passwords are encoded using MD4 -->
|
||||||
<!-- This is not ideal and only done to be compatible with NTLM -->
|
<!-- This is not ideal and only done to be compatible with NTLM -->
|
||||||
<!-- authentication against the default authentication mechanism. -->
|
<!-- authentication against the default authentication mechanism. -->
|
||||||
|
@@ -89,6 +89,8 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="adminConsoleAuthenticator" class="org.alfresco.repo.security.authentication.external.DefaultAdminConsoleAuthenticator" />
|
||||||
|
|
||||||
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
||||||
<property name="nodeService" ref="nodeService" />
|
<property name="nodeService" ref="nodeService" />
|
||||||
<property name="authorityService" ref="authorityService" />
|
<property name="authorityService" ref="authorityService" />
|
||||||
|
@@ -158,6 +158,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="adminConsoleAuthenticationCookiesService" class="org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesService">
|
||||||
|
<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">
|
||||||
|
<property name="active">
|
||||||
|
<value>${identity-service.authentication.enabled}</value>
|
||||||
|
</property>
|
||||||
|
<property name="identityServiceFacade">
|
||||||
|
<ref bean="identityServiceFacade"/>
|
||||||
|
</property>
|
||||||
|
<property name="cookiesService">
|
||||||
|
<ref bean="adminConsoleAuthenticationCookiesService" />
|
||||||
|
</property>
|
||||||
|
<property name="remoteUserMapper">
|
||||||
|
<ref bean="remoteUserMapper" />
|
||||||
|
</property>
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
|
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
|
||||||
<constructor-arg ref="PersonService"/>
|
<constructor-arg ref="PersonService"/>
|
||||||
<constructor-arg ref="identityServiceFacade"/>
|
<constructor-arg ref="identityServiceFacade"/>
|
||||||
|
@@ -29,6 +29,9 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
|
|||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
|
||||||
import org.alfresco.util.testing.category.DBTests;
|
import org.alfresco.util.testing.category.DBTests;
|
||||||
import org.alfresco.util.testing.category.NonBuildTests;
|
import org.alfresco.util.testing.category.NonBuildTests;
|
||||||
import org.junit.experimental.categories.Categories;
|
import org.junit.experimental.categories.Categories;
|
||||||
@@ -145,6 +148,9 @@ import org.junit.runners.Suite;
|
|||||||
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
||||||
SpringBasedIdentityServiceFacadeUnitTest.class,
|
SpringBasedIdentityServiceFacadeUnitTest.class,
|
||||||
IdentityServiceJITProvisioningHandlerUnitTest.class,
|
IdentityServiceJITProvisioningHandlerUnitTest.class,
|
||||||
|
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
|
||||||
|
AdminConsoleHttpServletRequestWrapperUnitTest.class,
|
||||||
|
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
|
||||||
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
||||||
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
||||||
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
||||||
|
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.alfresco.repo.admin.SysAdminParams;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
|
||||||
|
public class AdminConsoleAuthenticationCookiesServiceUnitTest
|
||||||
|
{
|
||||||
|
private static final int DEFAULT_COOKIE_LIFETIME = 86400;
|
||||||
|
private static final String COOKIE_NAME = "cookie";
|
||||||
|
private static final String COOKIE_VALUE = "value";
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
@Mock
|
||||||
|
private HttpServletResponse response;
|
||||||
|
@Mock
|
||||||
|
private SysAdminParams sysAdminParams;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Cookie> cookieCaptor;
|
||||||
|
private AdminConsoleAuthenticationCookiesService cookiesService;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp()
|
||||||
|
{
|
||||||
|
initMocks(this);
|
||||||
|
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, DEFAULT_COOKIE_LIFETIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieShouldBeFoundInRequestThatContainsIt()
|
||||||
|
{
|
||||||
|
when(request.getCookies()).thenReturn(new Cookie[] { new Cookie(COOKIE_NAME, COOKIE_VALUE) });
|
||||||
|
|
||||||
|
String cookie = cookiesService.getCookie(COOKIE_NAME, request);
|
||||||
|
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertEquals("The cookie's value should match", COOKIE_VALUE, cookie);
|
||||||
|
verify(request).getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieShouldNotBeFoundInRequestThatDoesNotContainIt()
|
||||||
|
{
|
||||||
|
when(request.getCookies()).thenReturn(new Cookie[] { new Cookie(COOKIE_NAME, COOKIE_VALUE) });
|
||||||
|
|
||||||
|
assertNull("The cookie should be null", cookiesService.getCookie("non-contained-cookie", request));
|
||||||
|
|
||||||
|
verify(request).getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieShouldNotBeFoundInRequestWithoutCookies()
|
||||||
|
{
|
||||||
|
when(request.getCookies()).thenReturn(null);
|
||||||
|
|
||||||
|
assertNull("The cookie should be null", cookiesService.getCookie(COOKIE_NAME, request));
|
||||||
|
|
||||||
|
verify(request).getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieShouldBeAddedToTheResponseWithDefaultParams()
|
||||||
|
{
|
||||||
|
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("http");
|
||||||
|
|
||||||
|
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
|
||||||
|
|
||||||
|
verify(sysAdminParams).getAlfrescoProtocol();
|
||||||
|
verify(response).addCookie(cookieCaptor.capture());
|
||||||
|
|
||||||
|
Cookie cookie = cookieCaptor.getValue();
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertEquals("Cookie's name should match", COOKIE_NAME, cookie.getName());
|
||||||
|
assertEquals("Cookie's value should match", COOKIE_VALUE, cookie.getValue());
|
||||||
|
assertEquals("Cookie's path should be the root", "/", cookie.getPath());
|
||||||
|
assertEquals("Cookie's maxAge should match the default lifetime", DEFAULT_COOKIE_LIFETIME, cookie.getMaxAge());
|
||||||
|
assertFalse("Cookie's secure flag should be false", cookie.getSecure());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void secureCookieShouldBeAddedToTheResponseWhenAlfrescoProtocolIsHttps()
|
||||||
|
{
|
||||||
|
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
|
||||||
|
|
||||||
|
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
|
||||||
|
|
||||||
|
verify(sysAdminParams).getAlfrescoProtocol();
|
||||||
|
verify(response).addCookie(cookieCaptor.capture());
|
||||||
|
|
||||||
|
Cookie cookie = cookieCaptor.getValue();
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertTrue("Cookie's secure flag should be true", cookie.getSecure());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieWithCustomMaxAgeShouldBeAddedToTheResponse()
|
||||||
|
{
|
||||||
|
int customMaxAge = 60;
|
||||||
|
cookiesService = new AdminConsoleAuthenticationCookiesService(sysAdminParams, customMaxAge);
|
||||||
|
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
|
||||||
|
|
||||||
|
cookiesService.addCookie(COOKIE_NAME, COOKIE_VALUE, response);
|
||||||
|
|
||||||
|
verify(sysAdminParams).getAlfrescoProtocol();
|
||||||
|
verify(response).addCookie(cookieCaptor.capture());
|
||||||
|
|
||||||
|
Cookie cookie = cookieCaptor.getValue();
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertEquals("Cookie's maxAge should match the custom lifetime", customMaxAge, cookie.getMaxAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cookieShouldBeReset()
|
||||||
|
{
|
||||||
|
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("http");
|
||||||
|
|
||||||
|
cookiesService.resetCookie(COOKIE_NAME, response);
|
||||||
|
|
||||||
|
verify(sysAdminParams).getAlfrescoProtocol();
|
||||||
|
verify(response).addCookie(cookieCaptor.capture());
|
||||||
|
|
||||||
|
Cookie cookie = cookieCaptor.getValue();
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertEquals("Cookie's name should match", COOKIE_NAME, cookie.getName());
|
||||||
|
assertEquals("Cookie's value should be reset", "", cookie.getValue());
|
||||||
|
assertEquals("Cookie's path should be the root", "/", cookie.getPath());
|
||||||
|
assertEquals("Cookie's maxAge should be 0", 0, cookie.getMaxAge());
|
||||||
|
assertFalse("Cookie's secure flag should be false", cookie.getSecure());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void secureCookieShouldBeReset()
|
||||||
|
{
|
||||||
|
when(sysAdminParams.getAlfrescoProtocol()).thenReturn("https");
|
||||||
|
|
||||||
|
cookiesService.resetCookie(COOKIE_NAME, response);
|
||||||
|
|
||||||
|
verify(sysAdminParams).getAlfrescoProtocol();
|
||||||
|
verify(response).addCookie(cookieCaptor.capture());
|
||||||
|
|
||||||
|
Cookie cookie = cookieCaptor.getValue();
|
||||||
|
assertNotNull("The cookie should not be null", cookie);
|
||||||
|
assertTrue("Cookie's secure flag should be true", cookie.getSecure());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
import static java.util.Collections.enumeration;
|
||||||
|
import static java.util.Collections.list;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.alfresco.error.AlfrescoRuntimeException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.UseDiamondOperator")
|
||||||
|
public class AdminConsoleHttpServletRequestWrapperUnitTest
|
||||||
|
{
|
||||||
|
|
||||||
|
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>()
|
||||||
|
{{
|
||||||
|
put(DEFAULT_HEADER, DEFAULT_HEADER_VALUE);
|
||||||
|
}};
|
||||||
|
private static final Map<String, String> ADDITIONAL_HEADERS = new HashMap<String, String>()
|
||||||
|
{{
|
||||||
|
put(ADDITIONAL_HEADER, ADDITIONAL_HEADER_VALUE);
|
||||||
|
}};
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
private AdminConsoleHttpServletRequestWrapper requestWrapper;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp()
|
||||||
|
{
|
||||||
|
initMocks(this);
|
||||||
|
requestWrapper = new AdminConsoleHttpServletRequestWrapper(ADDITIONAL_HEADERS, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AlfrescoRuntimeException.class)
|
||||||
|
public void wrapperShouldNotBeInstancedWithoutAdditionalHeaders()
|
||||||
|
{
|
||||||
|
new AdminConsoleHttpServletRequestWrapper(null, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void wrapperShouldNotBeInstancedWithoutRequestsToWrap()
|
||||||
|
{
|
||||||
|
new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnAdditionalHeaderNamesOnTopOfDefaultOnes()
|
||||||
|
{
|
||||||
|
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
|
||||||
|
|
||||||
|
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
|
||||||
|
assertNotNull("headerNames should not be null", headerNames);
|
||||||
|
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
|
||||||
|
|
||||||
|
List<String> headers = list(headerNames);
|
||||||
|
assertEquals("There should be 2 headers", 2, headers.size());
|
||||||
|
assertTrue("The default header should be included", headers.contains(DEFAULT_HEADER));
|
||||||
|
assertTrue("The additional header should be included", headers.contains(ADDITIONAL_HEADER));
|
||||||
|
|
||||||
|
verify(request).getHeaderNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnDefaultHeaderNamesIfNoAdditionalHeaders()
|
||||||
|
{
|
||||||
|
when(request.getHeaderNames()).thenReturn(enumeration(DEFAULT_HEADERS.keySet()));
|
||||||
|
|
||||||
|
requestWrapper = new AdminConsoleHttpServletRequestWrapper(new HashMap<>(), request);
|
||||||
|
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
|
||||||
|
assertNotNull("headerNames should not be null", headerNames);
|
||||||
|
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
|
||||||
|
assertEquals("The returned header should be the default header", DEFAULT_HEADER, headerNames.nextElement());
|
||||||
|
assertFalse("There should be no additional headers", headerNames.hasMoreElements());
|
||||||
|
|
||||||
|
verify(request).getHeaderNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnAdditionalHeaderNamesIfNoDefaultHeaders()
|
||||||
|
{
|
||||||
|
when(request.getHeaderNames()).thenReturn(null);
|
||||||
|
|
||||||
|
Enumeration<String> headerNames = requestWrapper.getHeaderNames();
|
||||||
|
assertNotNull("headerNames should not be null", headerNames);
|
||||||
|
assertTrue("headerNames should not be empty", headerNames.hasMoreElements());
|
||||||
|
assertEquals("The returned header should be the additional header", ADDITIONAL_HEADER,
|
||||||
|
headerNames.nextElement());
|
||||||
|
assertFalse("There should be no more headers", headerNames.hasMoreElements());
|
||||||
|
|
||||||
|
verify(request).getHeaderNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnDefaultHeaderValues()
|
||||||
|
{
|
||||||
|
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
|
||||||
|
|
||||||
|
String header = requestWrapper.getHeader(DEFAULT_HEADER);
|
||||||
|
assertEquals("The header should be the default one", DEFAULT_HEADER_VALUE, header);
|
||||||
|
|
||||||
|
verify(request).getHeader(DEFAULT_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnAdditionalHeaderValues()
|
||||||
|
{
|
||||||
|
String header = requestWrapper.getHeader(ADDITIONAL_HEADER);
|
||||||
|
assertEquals("The header should be the additional one", ADDITIONAL_HEADER_VALUE, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldPreferAdditionalHeaderValuesToDefaultOnes()
|
||||||
|
{
|
||||||
|
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
|
||||||
|
|
||||||
|
String overrideHeaderValue = "override";
|
||||||
|
Map<String, String> overrideHeaders = new HashMap<>();
|
||||||
|
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
|
||||||
|
|
||||||
|
requestWrapper = new AdminConsoleHttpServletRequestWrapper(overrideHeaders, request);
|
||||||
|
String header = requestWrapper.getHeader(DEFAULT_HEADER);
|
||||||
|
assertEquals("The header should have the overridden value", overrideHeaderValue, header);
|
||||||
|
|
||||||
|
verify(request).getHeader(DEFAULT_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnDefaultHeaderEnumeration()
|
||||||
|
{
|
||||||
|
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
|
||||||
|
|
||||||
|
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());
|
||||||
|
assertEquals("The header should be the default one", DEFAULT_HEADER_VALUE, headers.nextElement());
|
||||||
|
assertFalse("There should be no more headers", headers.hasMoreElements());
|
||||||
|
|
||||||
|
verify(request).getHeader(DEFAULT_HEADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldReturnAdditionalHeaderEnumeration()
|
||||||
|
{
|
||||||
|
Enumeration<String> headers = requestWrapper.getHeaders(ADDITIONAL_HEADER);
|
||||||
|
assertNotNull("The headers enumeration should not be null", headers);
|
||||||
|
assertTrue("The headers enumeration should not be empty", headers.hasMoreElements());
|
||||||
|
assertEquals("The header should be the additional one", ADDITIONAL_HEADER_VALUE, headers.nextElement());
|
||||||
|
assertFalse("There should be no more headers", headers.hasMoreElements());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapperShouldPreferAdditionalHeaderEnumerationValuesToDefaultOnes()
|
||||||
|
{
|
||||||
|
when(request.getHeader(DEFAULT_HEADER)).thenReturn(DEFAULT_HEADER_VALUE);
|
||||||
|
|
||||||
|
String overrideHeaderValue = "override";
|
||||||
|
Map<String, String> overrideHeaders = new HashMap<>();
|
||||||
|
overrideHeaders.put(DEFAULT_HEADER, overrideHeaderValue);
|
||||||
|
|
||||||
|
requestWrapper = new AdminConsoleHttpServletRequestWrapper(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());
|
||||||
|
assertEquals("The header should be the overridden one", overrideHeaderValue, headers.nextElement());
|
||||||
|
assertFalse("There should be no more headers", headers.hasMoreElements());
|
||||||
|
|
||||||
|
verify(request).getHeader(DEFAULT_HEADER);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 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.admin;
|
||||||
|
|
||||||
|
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 jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
@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";
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
HttpServletRequest request;
|
||||||
|
@Mock
|
||||||
|
HttpServletResponse response;
|
||||||
|
@Mock
|
||||||
|
IdentityServiceFacade identityServiceFacade;
|
||||||
|
@Mock
|
||||||
|
AdminConsoleAuthenticationCookiesService cookiesService;
|
||||||
|
@Mock
|
||||||
|
RemoteUserMapper remoteUserMapper;
|
||||||
|
@Mock
|
||||||
|
AccessTokenAuthorization accessTokenAuthorization;
|
||||||
|
@Mock
|
||||||
|
AccessToken accessToken;
|
||||||
|
@Captor
|
||||||
|
ArgumentCaptor<AdminConsoleHttpServletRequestWrapper> requestCaptor;
|
||||||
|
|
||||||
|
IdentityServiceAdminConsoleAuthenticator authenticator;
|
||||||
|
|
||||||
|
StringBuffer adminConsoleURL = new StringBuffer("http://localhost:8080/admin-console");
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup()
|
||||||
|
{
|
||||||
|
initMocks(this);
|
||||||
|
ClientRegistration clientRegistration = mock(ClientRegistration.class);
|
||||||
|
ProviderDetails providerDetails = mock(ProviderDetails.class);
|
||||||
|
when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
|
||||||
|
when(clientRegistration.getClientId()).thenReturn("alfresco");
|
||||||
|
when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
|
||||||
|
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
|
||||||
|
when(request.getRequestURL()).thenReturn(adminConsoleURL);
|
||||||
|
when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
|
||||||
|
|
||||||
|
authenticator = new IdentityServiceAdminConsoleAuthenticator();
|
||||||
|
authenticator.setActive(true);
|
||||||
|
authenticator.setIdentityServiceFacade(identityServiceFacade);
|
||||||
|
authenticator.setCookiesService(cookiesService);
|
||||||
|
authenticator.setRemoteUserMapper(remoteUserMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.getAdminConsoleUser(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.getAdminConsoleUser(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 shouldCallAuthChallenge() throws IOException
|
||||||
|
{
|
||||||
|
String authenticationRequest = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=" + adminConsoleURL
|
||||||
|
+ "&response_type=code&scope=openid";
|
||||||
|
authenticator.requestAuthentication(request, response);
|
||||||
|
|
||||||
|
verify(response).sendRedirect(authenticationRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.getAdminConsoleUser(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", adminConsoleURL.toString())))
|
||||||
|
.thenReturn(accessTokenAuthorization);
|
||||||
|
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
|
||||||
|
|
||||||
|
String username = authenticator.getAdminConsoleUser(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.getAdminConsoleUser(request, response);
|
||||||
|
|
||||||
|
assertEquals("admin", username);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user