mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS- 4752 keycloak migration - resource server role (#1818)
This commit is contained in:
@@ -529,7 +529,7 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest
|
|||||||
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
|
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
|
||||||
interceptingRemoteUserMapper.setActive(true);
|
interceptingRemoteUserMapper.setActive(true);
|
||||||
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
|
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
|
||||||
interceptingRemoteUserMapper.setIdentityServiceDeployment(null);
|
interceptingRemoteUserMapper.setIdentityServiceFacade(null);
|
||||||
interceptingRemoteUserMapper.setUserIdToReturn(user2);
|
interceptingRemoteUserMapper.setUserIdToReturn(user2);
|
||||||
remoteUserMapper = interceptingRemoteUserMapper;
|
remoteUserMapper = interceptingRemoteUserMapper;
|
||||||
}
|
}
|
||||||
|
@@ -397,6 +397,14 @@
|
|||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-oauth2-client</artifactId>
|
<artifactId>spring-security-oauth2-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.quartz-scheduler</groupId>
|
<groupId>org.quartz-scheduler</groupId>
|
||||||
<artifactId>quartz</artifactId>
|
<artifactId>quartz</artifactId>
|
||||||
|
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* #%L
|
|
||||||
* Alfresco Repository
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2005 - 2016 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;
|
|
||||||
|
|
||||||
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.OIDCAuthenticationError.Reason;
|
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends the Keycloak BearerTokenRequestAuthenticator class to capture the error description
|
|
||||||
* when token valiation fails.
|
|
||||||
*
|
|
||||||
* @author Gavin Cornwell
|
|
||||||
*/
|
|
||||||
public class AlfrescoBearerTokenRequestAuthenticator extends BearerTokenRequestAuthenticator
|
|
||||||
{
|
|
||||||
private String validationFailureDescription;
|
|
||||||
|
|
||||||
public AlfrescoBearerTokenRequestAuthenticator(KeycloakDeployment deployment)
|
|
||||||
{
|
|
||||||
super(deployment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getValidationFailureDescription()
|
|
||||||
{
|
|
||||||
return this.validationFailureDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AuthChallenge challengeResponse(HttpFacade facade, Reason reason, String error, String description)
|
|
||||||
{
|
|
||||||
this.validationFailureDescription = description;
|
|
||||||
|
|
||||||
return super.challengeResponse(facade, reason, error, description);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -28,32 +28,32 @@ package org.alfresco.repo.security.authentication.identityservice;
|
|||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Authenticates a user against Identity Service (Keycloak).
|
* Authenticates a user against Identity Service (Keycloak/Authorization Server).
|
||||||
* {@link OAuth2Client} is used to verify provided user credentials. User is set as the current user if the user
|
* {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the
|
||||||
* credentials are valid.
|
* user credentials are valid.
|
||||||
* <br>
|
* <br>
|
||||||
* The {@link IdentityServiceAuthenticationComponent#oAuth2Client} can be null in which case this authenticator will
|
* The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator
|
||||||
* just fall through to the next one in the chain.
|
* will just fall through to the next one in the chain.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
|
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
|
||||||
{
|
{
|
||||||
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
|
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
|
||||||
/** client used to authenticate user credentials against Authorization Server **/
|
/** client used to authenticate user credentials against Authorization Server **/
|
||||||
private OAuth2Client oAuth2Client;
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
/** enabled flag for the identity service subsystem**/
|
/** enabled flag for the identity service subsystem**/
|
||||||
private boolean active;
|
private boolean active;
|
||||||
private boolean allowGuestLogin;
|
private boolean allowGuestLogin;
|
||||||
|
|
||||||
public void setOAuth2Client(OAuth2Client oAuth2Client)
|
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
|
||||||
{
|
{
|
||||||
this.oAuth2Client = oAuth2Client;
|
this.identityServiceFacade = identityServiceFacade;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAllowGuestLogin(boolean allowGuestLogin)
|
public void setAllowGuestLogin(boolean allowGuestLogin)
|
||||||
@@ -63,21 +63,20 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
|||||||
|
|
||||||
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
|
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
|
||||||
{
|
{
|
||||||
|
if (identityServiceFacade == null)
|
||||||
if (oAuth2Client == null)
|
|
||||||
{
|
{
|
||||||
if (LOGGER.isDebugEnabled())
|
if (LOGGER.isDebugEnabled())
|
||||||
{
|
{
|
||||||
LOGGER.debug("OAuth2Client was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
|
LOGGER.debug("IdentityServiceFacade was not set, possibly due to the 'identity-service.authentication.enable-username-password-authentication=false' property.");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new AuthenticationException("User not authenticated because OAuth2Client was not set.");
|
throw new AuthenticationException("User not authenticated because IdentityServiceFacade was not set.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Attempt to verify user credentials
|
// Attempt to verify user credentials
|
||||||
oAuth2Client.verifyCredentials(userName, new String(password));
|
identityServiceFacade.verifyCredentials(userName, new String(password));
|
||||||
|
|
||||||
// Verification was successful so treat as authenticated user
|
// Verification was successful so treat as authenticated user
|
||||||
setCurrentUser(userName);
|
setCurrentUser(userName);
|
||||||
@@ -108,32 +107,4 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
|||||||
{
|
{
|
||||||
return allowGuestLogin;
|
return allowGuestLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstraction for acting as an OAuth2 Client
|
|
||||||
*/
|
|
||||||
interface OAuth2Client
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The OAuth2's Client role is only used to verify the user credentials (Resource Owner Password
|
|
||||||
* Credentials Flow) this is why there is an explicit method for verifying these.
|
|
||||||
* @param userName user's name
|
|
||||||
* @param password user's password
|
|
||||||
* @throws CredentialsVerificationException when the verification failed or couldn't be performed
|
|
||||||
*/
|
|
||||||
void verifyCredentials(String userName, String password);
|
|
||||||
|
|
||||||
class CredentialsVerificationException extends RuntimeException
|
|
||||||
{
|
|
||||||
CredentialsVerificationException(String message)
|
|
||||||
{
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
CredentialsVerificationException(String message, Throwable cause)
|
|
||||||
{
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* #%L
|
|
||||||
* Alfresco Repository
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2005 - 2016 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;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.keycloak.adapters.HttpClientBuilder;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of a KeycloakDeployment object for communicating with the Identity Service.
|
|
||||||
*
|
|
||||||
* @author Gavin Cornwell
|
|
||||||
*/
|
|
||||||
public class IdentityServiceDeploymentFactoryBean implements FactoryBean<KeycloakDeployment>
|
|
||||||
{
|
|
||||||
private static Log logger = LogFactory.getLog(IdentityServiceDeploymentFactoryBean.class);
|
|
||||||
|
|
||||||
private IdentityServiceConfig identityServiceConfig;
|
|
||||||
|
|
||||||
public void setIdentityServiceConfig(IdentityServiceConfig config)
|
|
||||||
{
|
|
||||||
this.identityServiceConfig = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeycloakDeployment getObject() throws Exception
|
|
||||||
{
|
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(this.identityServiceConfig);
|
|
||||||
|
|
||||||
// Set client with custom timeout values if client was created by the KeycloakDeploymentBuilder.
|
|
||||||
// This can be removed if the future versions of Keycloak accept timeout values through the config.
|
|
||||||
if (deployment.getClient() != null)
|
|
||||||
{
|
|
||||||
int connectionTimeout = identityServiceConfig.getClientConnectionTimeout();
|
|
||||||
int socketTimeout = identityServiceConfig.getClientSocketTimeout();
|
|
||||||
HttpClient client = new HttpClientBuilder()
|
|
||||||
.establishConnectionTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
|
|
||||||
.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
|
|
||||||
.build(this.identityServiceConfig);
|
|
||||||
deployment.setClient(client);
|
|
||||||
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("Created HttpClient for Keycloak deployment with connection timeout: "+ connectionTimeout + " ms, socket timeout: "+ socketTimeout+" ms.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("HttpClient for Keycloak deployment was not set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger.isInfoEnabled())
|
|
||||||
{
|
|
||||||
logger.info("Keycloak JWKS URL: " + deployment.getJwksUrl());
|
|
||||||
logger.info("Keycloak Realm: " + deployment.getRealm());
|
|
||||||
logger.info("Keycloak Client ID: " + deployment.getResourceName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return deployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<KeycloakDeployment> getObjectType()
|
|
||||||
{
|
|
||||||
return KeycloakDeployment.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSingleton()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* #%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;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to interact with the Identity Service
|
||||||
|
*/
|
||||||
|
interface IdentityServiceFacade
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Verifies provided user credentials. The OAuth2's Client role is only used to verify the user credentials (Resource Owner Password
|
||||||
|
* Credentials Flow) this is why there is an explicit method for verifying these.
|
||||||
|
*
|
||||||
|
* @param username user's name
|
||||||
|
* @param password user's password
|
||||||
|
* @throws CredentialsVerificationException when the verification failed or couldn't be performed
|
||||||
|
*/
|
||||||
|
void verifyCredentials(String username, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts username from provided token
|
||||||
|
*
|
||||||
|
* @param token token representation
|
||||||
|
* @return possible username
|
||||||
|
*/
|
||||||
|
Optional<String> extractUsernameFromToken(String token);
|
||||||
|
|
||||||
|
class IdentityServiceFacadeException extends RuntimeException
|
||||||
|
{
|
||||||
|
IdentityServiceFacadeException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityServiceFacadeException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class CredentialsVerificationException extends IdentityServiceFacadeException
|
||||||
|
{
|
||||||
|
CredentialsVerificationException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
CredentialsVerificationException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TokenException extends IdentityServiceFacadeException
|
||||||
|
{
|
||||||
|
TokenException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,388 @@
|
|||||||
|
/*
|
||||||
|
* #%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;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
|
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder.PasswordGrantBuilder;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||||
|
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
||||||
|
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||||
|
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||||
|
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Creates an instance of {@link IdentityServiceFacade}. <br>
|
||||||
|
* This factory can return a null if it is disabled.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentityServiceFacade>
|
||||||
|
{
|
||||||
|
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
|
||||||
|
private boolean enabled;
|
||||||
|
private SpringBasedIdentityServiceFacadeFactory factory;
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled)
|
||||||
|
{
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
|
||||||
|
{
|
||||||
|
factory = new SpringBasedIdentityServiceFacadeFactory(identityServiceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityServiceFacade getObject() throws Exception
|
||||||
|
{
|
||||||
|
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
|
||||||
|
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LazyInstantiatingIdentityServiceFacade(factory::createIdentityServiceFacade);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getObjectType()
|
||||||
|
{
|
||||||
|
return IdentityServiceFacade.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleton()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IdentityServiceFacadeException authorizationServerCantBeUsedException(RuntimeException cause)
|
||||||
|
{
|
||||||
|
return new IdentityServiceFacadeException("Unable to use the Authorization Server.", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The target facade is created lazily to improve resiliency on Identity Service
|
||||||
|
// (Keycloak/Authorization Server) failures when Spring Context is starting up.
|
||||||
|
static class LazyInstantiatingIdentityServiceFacade implements IdentityServiceFacade
|
||||||
|
{
|
||||||
|
private final AtomicReference<IdentityServiceFacade> targetFacade = new AtomicReference<>();
|
||||||
|
private final Supplier<IdentityServiceFacade> targetFacadeCreator;
|
||||||
|
|
||||||
|
LazyInstantiatingIdentityServiceFacade(Supplier<IdentityServiceFacade> targetFacadeCreator)
|
||||||
|
{
|
||||||
|
this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verifyCredentials(String username, String password)
|
||||||
|
{
|
||||||
|
getTargetFacade().verifyCredentials(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> extractUsernameFromToken(String token)
|
||||||
|
{
|
||||||
|
return getTargetFacade().extractUsernameFromToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IdentityServiceFacade getTargetFacade()
|
||||||
|
{
|
||||||
|
return ofNullable(targetFacade.get())
|
||||||
|
.orElseGet(() -> targetFacade.updateAndGet(prev ->
|
||||||
|
ofNullable(prev).orElseGet(this::createTargetFacade)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IdentityServiceFacade createTargetFacade()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return targetFacadeCreator.get();
|
||||||
|
}
|
||||||
|
catch (IdentityServiceFacadeException e)
|
||||||
|
{
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Failed to instantiate IdentityServiceFacade.", e);
|
||||||
|
throw authorizationServerCantBeUsedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SpringBasedIdentityServiceFacadeFactory
|
||||||
|
{
|
||||||
|
private static final long CLOCK_SKEW_MS = 0;
|
||||||
|
private final IdentityServiceConfig config;
|
||||||
|
|
||||||
|
SpringBasedIdentityServiceFacadeFactory(IdentityServiceConfig config)
|
||||||
|
{
|
||||||
|
this.config = Objects.requireNonNull(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IdentityServiceFacade createIdentityServiceFacade()
|
||||||
|
{
|
||||||
|
//Here we preserve the behaviour of previously used Keycloak Adapter
|
||||||
|
// * Client is authenticating itself using basic auth
|
||||||
|
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
|
||||||
|
// * There is no caching of authenticated clients (NoStoredAuthorizedClient)
|
||||||
|
// * There is only one Authorization Server/Client pair (SingleClientRegistration)
|
||||||
|
|
||||||
|
final RestTemplate restTemplate = createRestTemplate();
|
||||||
|
final ClientRegistration clientRegistration = createClientRegistration(restTemplate);
|
||||||
|
final OAuth2AuthorizedClientManager clientManager = createAuthorizedClientManager(restTemplate, clientRegistration);
|
||||||
|
final JwtDecoder jwtDecoder = createJwtDecoder(clientRegistration);
|
||||||
|
|
||||||
|
return new SpringBasedIdentityServiceFacade(clientManager, jwtDecoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RestTemplate createRestTemplate()
|
||||||
|
{
|
||||||
|
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||||
|
requestFactory.setConnectTimeout(config.getClientConnectionTimeout());
|
||||||
|
requestFactory.setReadTimeout(config.getClientSocketTimeout());
|
||||||
|
|
||||||
|
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
|
||||||
|
restTemplate.setRequestFactory(requestFactory);
|
||||||
|
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
||||||
|
|
||||||
|
return restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRegistration createClientRegistration(RestTemplate restTemplate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ClientRegistrations
|
||||||
|
.fromIssuerLocation(config.getIssuerUrl())
|
||||||
|
.clientId(config.getResource())
|
||||||
|
.clientSecret(config.getClientSecret())
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||||
|
.registrationId(SpringBasedIdentityServiceFacade.CLIENT_REGISTRATION_ID)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Failed to create ClientRegistration.", e);
|
||||||
|
throw authorizationServerCantBeUsedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2AuthorizedClientManager createAuthorizedClientManager(RestTemplate restTemplate, ClientRegistration clientRegistration)
|
||||||
|
{
|
||||||
|
final AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
|
||||||
|
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
|
||||||
|
new SingleClientRegistration(clientRegistration),
|
||||||
|
new NoStoredAuthorizedClient());
|
||||||
|
|
||||||
|
final Consumer<PasswordGrantBuilder> passwordGrantConfigurer = b -> {
|
||||||
|
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
||||||
|
client.setRestOperations(restTemplate);
|
||||||
|
b.accessTokenResponseClient(client);
|
||||||
|
|
||||||
|
b.clockSkew(Duration.of(CLOCK_SKEW_MS, ChronoUnit.MILLIS));
|
||||||
|
};
|
||||||
|
manager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
|
||||||
|
.password(passwordGrantConfigurer)
|
||||||
|
.build());
|
||||||
|
manager.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
|
||||||
|
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtDecoder createJwtDecoder(ClientRegistration clientRegistration)
|
||||||
|
{
|
||||||
|
final OidcIdTokenDecoderFactory decoderFactory = new OidcIdTokenDecoderFactory();
|
||||||
|
decoderFactory.setJwtValidatorFactory(c -> new DelegatingOAuth2TokenValidator<>(
|
||||||
|
new JwtTimestampValidator(Duration.of(CLOCK_SKEW_MS, ChronoUnit.MILLIS)),
|
||||||
|
new JwtIssuerValidator(c.getProviderDetails().getIssuerUri()),
|
||||||
|
new JwtClaimValidator<String>("typ", "Bearer"::equals),
|
||||||
|
new JwtClaimValidator<String>(JwtClaimNames.SUB, Objects::nonNull)
|
||||||
|
|
||||||
|
));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return decoderFactory.createDecoder(clientRegistration);
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Failed to create JwtDecoder.", e);
|
||||||
|
throw authorizationServerCantBeUsedException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoStoredAuthorizedClient implements OAuth2AuthorizedClientService
|
||||||
|
{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
|
||||||
|
{
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAuthorizedClient(String clientRegistrationId, String principalName)
|
||||||
|
{
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SingleClientRegistration implements ClientRegistrationRepository
|
||||||
|
{
|
||||||
|
private final ClientRegistration clientRegistration;
|
||||||
|
|
||||||
|
private SingleClientRegistration(ClientRegistration clientRegistration)
|
||||||
|
{
|
||||||
|
this.clientRegistration = requireNonNull(clientRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistration findByRegistrationId(String registrationId)
|
||||||
|
{
|
||||||
|
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||||
|
{
|
||||||
|
static final String CLIENT_REGISTRATION_ID = "ids";
|
||||||
|
private final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
|
||||||
|
private JwtDecoder jwtDecoder;
|
||||||
|
|
||||||
|
SpringBasedIdentityServiceFacade(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, JwtDecoder jwtDecoder)
|
||||||
|
{
|
||||||
|
this.oAuth2AuthorizedClientManager = requireNonNull(oAuth2AuthorizedClientManager);
|
||||||
|
this.jwtDecoder = requireNonNull(jwtDecoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verifyCredentials(String username, String password)
|
||||||
|
{
|
||||||
|
final OAuth2AuthorizedClient authorizedClient;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(username, password);
|
||||||
|
authorizedClient = oAuth2AuthorizedClientManager.authorize(authRequest);
|
||||||
|
}
|
||||||
|
catch (OAuth2AuthorizationException e)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
|
||||||
|
throw new CredentialsVerificationException("Authorization against the Authorization Server failed with " + e.getError() + ".", e);
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
|
||||||
|
throw new CredentialsVerificationException("Failed to authorize against Authorization Server.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedClient == null || authorizedClient.getAccessToken() == null)
|
||||||
|
{
|
||||||
|
throw new CredentialsVerificationException("Resource Owner Password Credentials is not supported by the Authorization Server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> extractUsernameFromToken(String token)
|
||||||
|
{
|
||||||
|
final Jwt validToken;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
validToken = jwtDecoder.decode(requireNonNull(token));
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
throw new TokenException("Failed to decode token. " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
if (LOGGER.isDebugEnabled())
|
||||||
|
{
|
||||||
|
LOGGER.debug("Bearer token outcome: " + validToken);
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(validToken)
|
||||||
|
.map(Jwt::getClaims)
|
||||||
|
.map(c -> c.get("preferred_username"))
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2AuthorizeRequest createPasswordCredentialsRequest(String userName, String password)
|
||||||
|
{
|
||||||
|
return OAuth2AuthorizeRequest
|
||||||
|
.withClientRegistrationId(CLIENT_REGISTRATION_ID)
|
||||||
|
.principal(userName)
|
||||||
|
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, userName)
|
||||||
|
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,107 +0,0 @@
|
|||||||
/*
|
|
||||||
* #%L
|
|
||||||
* Alfresco Repository
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2005 - 2016 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;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
import org.keycloak.adapters.servlet.ServletHttpFacade;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HttpFacade wrapper so we can re-use Keycloak authenticator classes.
|
|
||||||
*
|
|
||||||
* @author Gavin Cornwell
|
|
||||||
*/
|
|
||||||
public class IdentityServiceHttpFacade extends ServletHttpFacade
|
|
||||||
{
|
|
||||||
public IdentityServiceHttpFacade(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
super(request, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response getResponse()
|
|
||||||
{
|
|
||||||
// return our dummy NoOp implementation so we don't effect the ACS response
|
|
||||||
return new NoOpResponseFacade();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NoOp implementation of Keycloak Response interface.
|
|
||||||
*/
|
|
||||||
private class NoOpResponseFacade implements Response
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setStatus(int status)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addHeader(String name, String value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setHeader(String name, String value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resetCookie(String name, String path)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCookie(String name, String value, String path, String domain, int maxAge,
|
|
||||||
boolean secure, boolean httpOnly)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OutputStream getOutputStream()
|
|
||||||
{
|
|
||||||
return new ByteArrayOutputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendError(int code)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendError(int code, String message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void end()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2016 Alfresco Software Limited
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -27,17 +27,19 @@ package org.alfresco.repo.security.authentication.identityservice;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
import org.keycloak.adapters.spi.AuthOutcome;
|
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link RemoteUserMapper} implementation that detects and validates JWTs
|
* A {@link RemoteUserMapper} implementation that detects and validates JWTs
|
||||||
@@ -47,7 +49,7 @@ import org.keycloak.representations.AccessToken;
|
|||||||
*/
|
*/
|
||||||
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
|
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
|
||||||
{
|
{
|
||||||
private static Log logger = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
|
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
|
||||||
|
|
||||||
/** Is the mapper enabled */
|
/** Is the mapper enabled */
|
||||||
private boolean isEnabled;
|
private boolean isEnabled;
|
||||||
@@ -58,8 +60,8 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
/** The person service. */
|
/** The person service. */
|
||||||
private PersonService personService;
|
private PersonService personService;
|
||||||
|
|
||||||
/** The Keycloak deployment object */
|
private BearerTokenResolver bearerTokenResolver;
|
||||||
private KeycloakDeployment keycloakDeployment;
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the active flag
|
* Sets the active flag
|
||||||
@@ -92,57 +94,56 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
this.personService = personService;
|
this.personService = personService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIdentityServiceDeployment(KeycloakDeployment deployment)
|
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
|
||||||
{
|
{
|
||||||
this.keycloakDeployment = deployment;
|
this.bearerTokenResolver = bearerTokenResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
|
||||||
|
{
|
||||||
|
this.identityServiceFacade = identityServiceFacade;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(javax.servlet.http.HttpServletRequest)
|
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(javax.servlet.http.HttpServletRequest)
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public String getRemoteUser(HttpServletRequest request)
|
public String getRemoteUser(HttpServletRequest request)
|
||||||
{
|
{
|
||||||
|
LOGGER.trace("Retrieving username from http request...");
|
||||||
|
|
||||||
|
if (!this.isEnabled)
|
||||||
|
{
|
||||||
|
LOGGER.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (logger.isTraceEnabled())
|
|
||||||
{
|
|
||||||
logger.trace("Retrieving username from http request...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isEnabled)
|
|
||||||
{
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("IdentityServiceRemoteUserMapper is disabled, returning null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String headerUserId = extractUserFromHeader(request);
|
String headerUserId = extractUserFromHeader(request);
|
||||||
|
|
||||||
if (headerUserId != null)
|
if (headerUserId != null)
|
||||||
{
|
{
|
||||||
// Normalize the user ID taking into account case sensitivity settings
|
// Normalize the user ID taking into account case sensitivity settings
|
||||||
String normalizedUserId = normalizeUserId(headerUserId);
|
String normalizedUserId = normalizeUserId(headerUserId);
|
||||||
|
LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
|
||||||
if (logger.isTraceEnabled())
|
|
||||||
{
|
|
||||||
logger.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedUserId;
|
return normalizedUserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (TokenException e)
|
||||||
{
|
{
|
||||||
logger.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
|
if (!isValidationFailureSilent)
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("Failed to extract username from token: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
if (logger.isTraceEnabled())
|
catch (RuntimeException e)
|
||||||
{
|
{
|
||||||
logger.trace("Could not identify a userId. Returning null.");
|
LOGGER.error("Failed to authenticate user using IdentityServiceRemoteUserMapper: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
LOGGER.trace("Could not identify a userId. Returning null.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,57 +164,32 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
*/
|
*/
|
||||||
private String extractUserFromHeader(HttpServletRequest request)
|
private String extractUserFromHeader(HttpServletRequest request)
|
||||||
{
|
{
|
||||||
String userName = null;
|
|
||||||
|
|
||||||
IdentityServiceHttpFacade facade = new IdentityServiceHttpFacade(request);
|
|
||||||
|
|
||||||
// try authenticating with bearer token first
|
// try authenticating with bearer token first
|
||||||
if (logger.isDebugEnabled())
|
LOGGER.debug("Trying bearer token...");
|
||||||
|
|
||||||
|
final String bearerToken;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
logger.debug("Trying bearer token...");
|
bearerToken = bearerTokenResolver.resolve(request);
|
||||||
|
}
|
||||||
|
catch (OAuth2AuthenticationException e)
|
||||||
|
{
|
||||||
|
LOGGER.debug("Failed to resolve Bearer token.", e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
AlfrescoBearerTokenRequestAuthenticator tokenAuthenticator =
|
final Optional<String> possibleUsername = Optional.ofNullable(bearerToken)
|
||||||
new AlfrescoBearerTokenRequestAuthenticator(this.keycloakDeployment);
|
.flatMap(identityServiceFacade::extractUsernameFromToken);
|
||||||
AuthOutcome tokenOutcome = tokenAuthenticator.authenticate(facade);
|
if (possibleUsername.isEmpty())
|
||||||
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
{
|
||||||
logger.debug("Bearer token outcome: " + tokenOutcome);
|
LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenOutcome == AuthOutcome.FAILED && !isValidationFailureSilent)
|
String username = possibleUsername.get();
|
||||||
{
|
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username));
|
||||||
throw new AuthenticationException("Token validation failed: " +
|
|
||||||
tokenAuthenticator.getValidationFailureDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenOutcome == AuthOutcome.AUTHENTICATED)
|
return username;
|
||||||
{
|
|
||||||
userName = extractUserFromToken(tokenAuthenticator.getToken());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (logger.isDebugEnabled())
|
|
||||||
{
|
|
||||||
logger.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractUserFromToken(AccessToken jwt)
|
|
||||||
{
|
|
||||||
// retrieve the preferred_username claim
|
|
||||||
String userName = jwt.getPreferredUsername();
|
|
||||||
|
|
||||||
if (logger.isTraceEnabled())
|
|
||||||
{
|
|
||||||
logger.trace("Extracted username: " + AuthenticationUtil.maskUsername(userName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return userName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -238,9 +214,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
}
|
}
|
||||||
}, AuthenticationUtil.getSystemUserName());
|
}, AuthenticationUtil.getSystemUserName());
|
||||||
|
|
||||||
if (logger.isDebugEnabled())
|
if (LOGGER.isDebugEnabled())
|
||||||
{
|
{
|
||||||
logger.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
|
LOGGER.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalized == null ? userId : normalized;
|
return normalized == null ? userId : normalized;
|
||||||
|
@@ -1,270 +0,0 @@
|
|||||||
/*
|
|
||||||
* #%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;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
|
||||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
|
||||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder.PasswordGrantBuilder;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
|
||||||
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
|
||||||
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
|
||||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
||||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
|
||||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Creates an instance of {@link OAuth2Client}. <br>
|
|
||||||
* The creation of {@link OAuth2Client} requires connection to the Identity Service (Keycloak), disable this factory if
|
|
||||||
* the server cannot be reached. <br>
|
|
||||||
* This factory can return a null if it is disabled.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class OAuth2ClientFactoryBean implements FactoryBean<OAuth2Client>
|
|
||||||
{
|
|
||||||
|
|
||||||
private static final Log LOGGER = LogFactory.getLog(OAuth2ClientFactoryBean.class);
|
|
||||||
private IdentityServiceConfig identityServiceConfig;
|
|
||||||
private boolean enabled;
|
|
||||||
|
|
||||||
public void setEnabled(boolean enabled)
|
|
||||||
{
|
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
|
|
||||||
{
|
|
||||||
this.identityServiceConfig = identityServiceConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OAuth2Client getObject() throws Exception
|
|
||||||
{
|
|
||||||
// The creation of the client can be disabled for testing or when the username/password authentication is not required,
|
|
||||||
// for instance when Keycloak is configured for 'bearer only' authentication or Direct Access Grants are disabled.
|
|
||||||
if (!enabled)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OAuth2AuthorizedClientManager isn't created upfront to make the code resilient to Identity Service being down.
|
|
||||||
// If it's down the Application Context will start and when it's back online it can be used.
|
|
||||||
return new SpringOAuth2Client(this::createOAuth2AuthorizedClientManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2AuthorizedClientManager createOAuth2AuthorizedClientManager()
|
|
||||||
{
|
|
||||||
//Here we preserve the behaviour of previously used Keycloak Adapter
|
|
||||||
// * Client is authenticating itself using basic auth
|
|
||||||
// * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
|
|
||||||
// * There is no caching of authenticated clients (NoStoredAuthorizedClient)
|
|
||||||
// * There is only one Authorization Server/Client pair (SingleClientRegistration)
|
|
||||||
|
|
||||||
final ClientRegistration clientRegistration = ClientRegistrations
|
|
||||||
.fromIssuerLocation(identityServiceConfig.getIssuerUrl())
|
|
||||||
.clientId(identityServiceConfig.getResource())
|
|
||||||
.clientSecret(identityServiceConfig.getClientSecret())
|
|
||||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
|
||||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
|
||||||
.registrationId(SpringOAuth2Client.CLIENT_REGISTRATION_ID)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
final AuthorizedClientServiceOAuth2AuthorizedClientManager oauth2 =
|
|
||||||
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
|
|
||||||
new SingleClientRegistration(clientRegistration),
|
|
||||||
new NoStoredAuthorizedClient());
|
|
||||||
oauth2.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
|
|
||||||
oauth2.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
|
|
||||||
.password(this::configureTimeouts)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOGGER.debug(" Created OAuth2 Client");
|
|
||||||
LOGGER.debug(" OAuth2 Issuer URL: " + clientRegistration.getProviderDetails().getIssuerUri());
|
|
||||||
LOGGER.debug(" OAuth2 ClientId: " + clientRegistration.getClientId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return oauth2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureTimeouts(PasswordGrantBuilder builder)
|
|
||||||
{
|
|
||||||
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
|
||||||
requestFactory.setConnectTimeout(identityServiceConfig.getClientConnectionTimeout());
|
|
||||||
requestFactory.setReadTimeout(identityServiceConfig.getClientSocketTimeout());
|
|
||||||
|
|
||||||
final RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
|
|
||||||
restTemplate.setRequestFactory(requestFactory);
|
|
||||||
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
|
||||||
|
|
||||||
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
|
||||||
client.setRestOperations(restTemplate);
|
|
||||||
|
|
||||||
builder.accessTokenResponseClient(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<?> getObjectType()
|
|
||||||
{
|
|
||||||
return OAuth2Client.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSingleton()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class SpringOAuth2Client implements OAuth2Client
|
|
||||||
{
|
|
||||||
private static final String CLIENT_REGISTRATION_ID = "ids";
|
|
||||||
private final Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier;
|
|
||||||
private final AtomicReference<OAuth2AuthorizedClientManager> authorizedClientManager = new AtomicReference<>();
|
|
||||||
|
|
||||||
public SpringOAuth2Client(Supplier<OAuth2AuthorizedClientManager> authorizedClientManagerSupplier)
|
|
||||||
{
|
|
||||||
this.authorizedClientManagerSupplier = Objects.requireNonNull(authorizedClientManagerSupplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void verifyCredentials(String userName, String password)
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizedClientManager clientManager;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
clientManager = getAuthorizedClientManager();
|
|
||||||
}
|
|
||||||
catch (RuntimeException e)
|
|
||||||
{
|
|
||||||
LOGGER.warn("Failed to instantiate OAuth2AuthorizedClientManager.", e);
|
|
||||||
throw new CredentialsVerificationException("Unable to use the Authorization Server.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
final OAuth2AuthorizedClient authorizedClient;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(userName, password);
|
|
||||||
authorizedClient = clientManager.authorize(authRequest);
|
|
||||||
}
|
|
||||||
catch (OAuth2AuthorizationException e)
|
|
||||||
{
|
|
||||||
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
|
|
||||||
throw new CredentialsVerificationException("Authorization against the Authorization Server failed with " + e.getError() + ".", e);
|
|
||||||
}
|
|
||||||
catch (RuntimeException e)
|
|
||||||
{
|
|
||||||
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
|
|
||||||
throw new CredentialsVerificationException("Failed to authorize against Authorization Server.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authorizedClient == null || authorizedClient.getAccessToken() == null)
|
|
||||||
{
|
|
||||||
throw new CredentialsVerificationException("Resource Owner Password Credentials is not supported by the Authorization Server.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2AuthorizedClientManager getAuthorizedClientManager()
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizedClientManager current = authorizedClientManager.get();
|
|
||||||
if (current != null)
|
|
||||||
{
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
return authorizedClientManager
|
|
||||||
.updateAndGet(prev -> prev != null ? prev : authorizedClientManagerSupplier.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
private OAuth2AuthorizeRequest createPasswordCredentialsRequest(String userName, String password)
|
|
||||||
{
|
|
||||||
return OAuth2AuthorizeRequest
|
|
||||||
.withClientRegistrationId(CLIENT_REGISTRATION_ID)
|
|
||||||
.principal(userName)
|
|
||||||
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, userName)
|
|
||||||
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NoStoredAuthorizedClient implements OAuth2AuthorizedClientService
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
|
|
||||||
{
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAuthorizedClient(String clientRegistrationId, String principalName)
|
|
||||||
{
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SingleClientRegistration implements ClientRegistrationRepository
|
|
||||||
{
|
|
||||||
private final ClientRegistration clientRegistration;
|
|
||||||
|
|
||||||
private SingleClientRegistration(ClientRegistration clientRegistration)
|
|
||||||
{
|
|
||||||
this.clientRegistration = Objects.requireNonNull(clientRegistration);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ClientRegistration findByRegistrationId(String registrationId)
|
|
||||||
{
|
|
||||||
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -21,12 +21,12 @@
|
|||||||
<property name="allowGuestLogin">
|
<property name="allowGuestLogin">
|
||||||
<value>${identity-service.authentication.allowGuestLogin}</value>
|
<value>${identity-service.authentication.allowGuestLogin}</value>
|
||||||
</property>
|
</property>
|
||||||
<property name="oAuth2Client">
|
<property name="identityServiceFacade">
|
||||||
<ref bean="oAuth2Client"/>
|
<ref bean="identityServiceFacade"/>
|
||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="oAuth2Client" class="org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean">
|
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
|
||||||
<property name="identityServiceConfig">
|
<property name="identityServiceConfig">
|
||||||
<ref bean="identityServiceConfig" />
|
<ref bean="identityServiceConfig" />
|
||||||
</property>
|
</property>
|
||||||
@@ -205,12 +205,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="identityServiceDeployment" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceDeploymentFactoryBean">
|
|
||||||
<property name="identityServiceConfig">
|
|
||||||
<ref bean="identityServiceConfig" />
|
|
||||||
</property>
|
|
||||||
</bean>
|
|
||||||
|
|
||||||
<!-- Enable control over mapping between request and user ID -->
|
<!-- Enable control over mapping between request and user ID -->
|
||||||
<bean id="remoteUserMapper" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper">
|
<bean id="remoteUserMapper" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper">
|
||||||
<property name="active">
|
<property name="active">
|
||||||
@@ -222,8 +216,11 @@
|
|||||||
<property name="personService">
|
<property name="personService">
|
||||||
<ref bean="PersonService" />
|
<ref bean="PersonService" />
|
||||||
</property>
|
</property>
|
||||||
<property name="identityServiceDeployment">
|
<property name="bearerTokenResolver">
|
||||||
<ref bean="identityServiceDeployment" />
|
<bean class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver" />
|
||||||
|
</property>
|
||||||
|
<property name="identityServiceFacade">
|
||||||
|
<ref bean="identityServiceFacade" />
|
||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
@@ -25,6 +25,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco;
|
package org.alfresco;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
||||||
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;
|
||||||
@@ -136,7 +138,8 @@ import org.junit.runners.Suite;
|
|||||||
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
|
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
|
||||||
org.alfresco.util.BeanExtenderUnitTest.class,
|
org.alfresco.util.BeanExtenderUnitTest.class,
|
||||||
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
|
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
|
||||||
org.alfresco.repo.security.authentication.identityservice.SpringOAuth2ClientUnitTest.class,
|
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
||||||
|
SpringBasedIdentityServiceFacadeUnitTest.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,
|
||||||
|
@@ -34,8 +34,7 @@ import java.net.ConnectException;
|
|||||||
import org.alfresco.error.ExceptionStackUtil;
|
import org.alfresco.error.ExceptionStackUtil;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationContext;
|
import org.alfresco.repo.security.authentication.AuthenticationContext;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
|
|
||||||
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
|
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
|
||||||
import org.alfresco.service.cmr.repository.NodeService;
|
import org.alfresco.service.cmr.repository.NodeService;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
@@ -65,7 +64,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PersonService personService;
|
private PersonService personService;
|
||||||
|
|
||||||
private OAuth2Client mockOAuth2Client;
|
private IdentityServiceFacade mockIdentityServiceFacade;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp()
|
public void setUp()
|
||||||
@@ -76,8 +75,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
authComponent.setNodeService(nodeService);
|
authComponent.setNodeService(nodeService);
|
||||||
authComponent.setPersonService(personService);
|
authComponent.setPersonService(personService);
|
||||||
|
|
||||||
mockOAuth2Client = mock(OAuth2Client.class);
|
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
|
||||||
authComponent.setOAuth2Client(mockOAuth2Client);
|
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -90,7 +89,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
public void testAuthenticationFail()
|
public void testAuthenticationFail()
|
||||||
{
|
{
|
||||||
doThrow(new CredentialsVerificationException("Failed"))
|
doThrow(new CredentialsVerificationException("Failed"))
|
||||||
.when(mockOAuth2Client)
|
.when(mockIdentityServiceFacade)
|
||||||
.verifyCredentials("username", "password");
|
.verifyCredentials("username", "password");
|
||||||
|
|
||||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||||
@@ -100,7 +99,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
public void testAuthenticationFail_connectionException()
|
public void testAuthenticationFail_connectionException()
|
||||||
{
|
{
|
||||||
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
|
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
|
||||||
.when(mockOAuth2Client)
|
.when(mockIdentityServiceFacade)
|
||||||
.verifyCredentials("username", "password");
|
.verifyCredentials("username", "password");
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -119,7 +118,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
public void testAuthenticationFail_otherException()
|
public void testAuthenticationFail_otherException()
|
||||||
{
|
{
|
||||||
doThrow(new RuntimeException("Some other errors!"))
|
doThrow(new RuntimeException("Some other errors!"))
|
||||||
.when(mockOAuth2Client)
|
.when(mockIdentityServiceFacade)
|
||||||
.verifyCredentials("username", "password");
|
.verifyCredentials("username", "password");
|
||||||
|
|
||||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||||
@@ -128,7 +127,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
@Test
|
@Test
|
||||||
public void testAuthenticationPass()
|
public void testAuthenticationPass()
|
||||||
{
|
{
|
||||||
doNothing().when(mockOAuth2Client).verifyCredentials("username", "password");
|
doNothing().when(mockIdentityServiceFacade).verifyCredentials("username", "password");
|
||||||
|
|
||||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||||
|
|
||||||
@@ -137,9 +136,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test (expected= AuthenticationException.class)
|
@Test (expected= AuthenticationException.class)
|
||||||
public void testFallthroughWhenOAuth2ClientIsNull()
|
public void testFallthroughWhenIdentityServiceFacadeIsNull()
|
||||||
{
|
{
|
||||||
authComponent.setOAuth2Client(null);
|
authComponent.setIdentityServiceFacade(null);
|
||||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2016 Alfresco Software Limited
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -25,375 +25,87 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.KeyPairGenerator;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
import java.util.regex.Pattern;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.alfresco.repo.management.subsystems.AbstractChainedSubsystemTest;
|
import junit.framework.TestCase;
|
||||||
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
|
|
||||||
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
|
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
import org.alfresco.util.ApplicationContextHelper;
|
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.HttpResponse;
|
|
||||||
import org.apache.http.StatusLine;
|
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
|
|
||||||
import org.keycloak.common.util.Base64;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the Identity Service based authentication subsystem.
|
* Tests the Identity Service based authentication subsystem.
|
||||||
*
|
*
|
||||||
* @author Gavin Cornwell
|
* @author Gavin Cornwell
|
||||||
*/
|
*/
|
||||||
public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsystemTest
|
public class IdentityServiceRemoteUserMapperTest extends TestCase
|
||||||
{
|
{
|
||||||
private static final String REMOTE_USER_MAPPER_BEAN_NAME = "remoteUserMapper";
|
|
||||||
private static final String DEPLOYMENT_BEAN_NAME = "identityServiceDeployment";
|
|
||||||
private static final String CONFIG_BEAN_NAME = "identityServiceConfig";
|
|
||||||
|
|
||||||
private static final String TEST_USER_USERNAME = "testuser";
|
|
||||||
private static final String TEST_USER_EMAIL = "testuser@mail.com";
|
|
||||||
|
|
||||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
private static final String BEARER_PREFIX = "Bearer ";
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
private static final String BASIC_PREFIX = "Basic ";
|
|
||||||
|
|
||||||
private static final String CONFIG_SILENT_ERRORS = "identity-service.authentication.validation.failure.silent";
|
public void testValidToken()
|
||||||
|
|
||||||
private static final String PASSWORD_GRANT_RESPONSE = "{" +
|
|
||||||
"\"access_token\": \"%s\"," +
|
|
||||||
"\"expires_in\": 300," +
|
|
||||||
"\"refresh_expires_in\": 1800," +
|
|
||||||
"\"refresh_token\": \"%s\"," +
|
|
||||||
"\"token_type\": \"bearer\"," +
|
|
||||||
"\"not-before-policy\": 0," +
|
|
||||||
"\"session_state\": \"71c2c5ba-9c98-49fc-882f-dedcf80ee1b5\"}";
|
|
||||||
|
|
||||||
ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
|
|
||||||
DefaultChildApplicationContextManager childApplicationContextManager;
|
|
||||||
ChildApplicationContextFactory childApplicationContextFactory;
|
|
||||||
|
|
||||||
private KeyPair keyPair;
|
|
||||||
private IdentityServiceConfig identityServiceConfig;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setUp() throws Exception
|
|
||||||
{
|
{
|
||||||
// switch authentication to use token auth
|
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("VaLiD-ToKeN", () -> "johny"));
|
||||||
childApplicationContextManager = (DefaultChildApplicationContextManager) ctx.getBean("Authentication");
|
|
||||||
childApplicationContextManager.stop();
|
|
||||||
childApplicationContextManager.setProperty("chain", "identity-service1:identity-service");
|
|
||||||
childApplicationContextFactory = getChildApplicationContextFactory(childApplicationContextManager, "identity-service1");
|
|
||||||
|
|
||||||
// generate keys for test
|
HttpServletRequest mockRequest = createMockTokenRequest("VaLiD-ToKeN");
|
||||||
this.keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
|
||||||
|
|
||||||
// hardcode the realm public key in the deployment bean to stop it fetching keys
|
final String user = mapper.getRemoteUser(mockRequest);
|
||||||
applyHardcodedPublicKey(this.keyPair.getPublic());
|
assertEquals("johny", user);
|
||||||
|
|
||||||
// extract config
|
|
||||||
this.identityServiceConfig = (IdentityServiceConfig)childApplicationContextFactory.
|
|
||||||
getApplicationContext().getBean(CONFIG_BEAN_NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void testWrongTokenWithSilentValidation()
|
||||||
protected void tearDown() throws Exception
|
|
||||||
{
|
{
|
||||||
childApplicationContextManager.destroy();
|
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected ");}));
|
||||||
childApplicationContextManager = null;
|
mapper.setValidationFailureSilent(true);
|
||||||
childApplicationContextFactory = null;
|
|
||||||
|
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
|
||||||
|
|
||||||
|
final String user = mapper.getRemoteUser(mockRequest);
|
||||||
|
assertNull(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testKeycloakConfig() throws Exception
|
public void testWrongTokenWithoutSilentValidation()
|
||||||
{
|
{
|
||||||
//Get the host of the IDS test server
|
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected");}));
|
||||||
String ip = "localhost";
|
mapper.setValidationFailureSilent(false);
|
||||||
try {
|
|
||||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
|
||||||
while (interfaces.hasMoreElements()) {
|
|
||||||
NetworkInterface iface = interfaces.nextElement();
|
|
||||||
// filters out 127.0.0.1 and inactive interfaces
|
|
||||||
if (iface.isLoopback() || !iface.isUp())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Enumeration<InetAddress> addresses = iface.getInetAddresses();
|
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
|
||||||
while(addresses.hasMoreElements()) {
|
|
||||||
InetAddress addr = addresses.nextElement();
|
|
||||||
if(Pattern.matches("([0-9]{1,3}\\.){3}[0-9]{1,3}", addr.getHostAddress())){
|
|
||||||
ip = addr.getHostAddress();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SocketException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check string overrides
|
assertThatExceptionOfType(AuthenticationException.class)
|
||||||
assertEquals("identity-service.auth-server-url", "http://"+ip+":8999/auth",
|
.isThrownBy(() -> mapper.getRemoteUser(mockRequest))
|
||||||
this.identityServiceConfig.getAuthServerUrl());
|
.havingCause().withNoCause().withMessage("Expected");
|
||||||
|
|
||||||
assertEquals("identity-service.realm", "alfresco",
|
|
||||||
this.identityServiceConfig.getRealm());
|
|
||||||
|
|
||||||
assertEquals("identity-service.realm-public-key",
|
|
||||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l" +
|
|
||||||
"BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM" +
|
|
||||||
"uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W" +
|
|
||||||
"ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD" +
|
|
||||||
"P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j" +
|
|
||||||
"QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx" +
|
|
||||||
"jwIDAQAB",
|
|
||||||
this.identityServiceConfig.getRealmKey());
|
|
||||||
|
|
||||||
assertEquals("identity-service.ssl-required", "external",
|
|
||||||
this.identityServiceConfig.getSslRequired());
|
|
||||||
|
|
||||||
assertEquals("identity-service.resource", "test",
|
|
||||||
this.identityServiceConfig.getResource());
|
|
||||||
|
|
||||||
assertEquals("identity-service.cors-allowed-headers", "Authorization",
|
|
||||||
this.identityServiceConfig.getCorsAllowedHeaders());
|
|
||||||
|
|
||||||
assertEquals("identity-service.cors-allowed-methods", "POST, PUT, DELETE, GET",
|
|
||||||
this.identityServiceConfig.getCorsAllowedMethods());
|
|
||||||
|
|
||||||
assertEquals("identity-service.cors-exposed-headers", "WWW-Authenticate, My-custom-exposed-Header",
|
|
||||||
this.identityServiceConfig.getCorsExposedHeaders());
|
|
||||||
|
|
||||||
assertEquals("identity-service.truststore",
|
|
||||||
"classpath:/alfresco/subsystems/identityServiceAuthentication/keystore.jks",
|
|
||||||
this.identityServiceConfig.getTruststore());
|
|
||||||
|
|
||||||
assertEquals("identity-service.truststore-password", "password",
|
|
||||||
this.identityServiceConfig.getTruststorePassword());
|
|
||||||
|
|
||||||
assertEquals("identity-service.client-keystore",
|
|
||||||
"classpath:/alfresco/subsystems/identityServiceAuthentication/keystore.jks",
|
|
||||||
this.identityServiceConfig.getClientKeystore());
|
|
||||||
|
|
||||||
assertEquals("identity-service.client-keystore-password", "password",
|
|
||||||
this.identityServiceConfig.getClientKeystorePassword());
|
|
||||||
|
|
||||||
assertEquals("identity-service.client-key-password", "password",
|
|
||||||
this.identityServiceConfig.getClientKeyPassword());
|
|
||||||
|
|
||||||
assertEquals("identity-service.token-store", "SESSION",
|
|
||||||
this.identityServiceConfig.getTokenStore());
|
|
||||||
|
|
||||||
assertEquals("identity-service.principal-attribute", "preferred_username",
|
|
||||||
this.identityServiceConfig.getPrincipalAttribute());
|
|
||||||
|
|
||||||
// check number overrides
|
|
||||||
assertEquals("identity-service.confidential-port", 100,
|
|
||||||
this.identityServiceConfig.getConfidentialPort());
|
|
||||||
|
|
||||||
assertEquals("identity-service.cors-max-age", 1000,
|
|
||||||
this.identityServiceConfig.getCorsMaxAge());
|
|
||||||
|
|
||||||
assertEquals("identity-service.connection-pool-size", 5,
|
|
||||||
this.identityServiceConfig.getConnectionPoolSize());
|
|
||||||
|
|
||||||
assertEquals("identity-service.register-node-period", 50,
|
|
||||||
this.identityServiceConfig.getRegisterNodePeriod());
|
|
||||||
|
|
||||||
assertEquals("identity-service.token-minimum-time-to-live", 10,
|
|
||||||
this.identityServiceConfig.getTokenMinimumTimeToLive());
|
|
||||||
|
|
||||||
assertEquals("identity-service.min-time-between-jwks-requests", 60,
|
|
||||||
this.identityServiceConfig.getMinTimeBetweenJwksRequests());
|
|
||||||
|
|
||||||
assertEquals("identity-service.public-key-cache-ttl", 3600,
|
|
||||||
this.identityServiceConfig.getPublicKeyCacheTtl());
|
|
||||||
|
|
||||||
assertEquals("identity-service.client-connection-timeout", 3000,
|
|
||||||
this.identityServiceConfig.getClientConnectionTimeout());
|
|
||||||
|
|
||||||
assertEquals("identity-service.client-socket-timeout", 1000,
|
|
||||||
this.identityServiceConfig.getClientSocketTimeout());
|
|
||||||
|
|
||||||
// check boolean overrides
|
|
||||||
assertFalse("identity-service.public-client",
|
|
||||||
this.identityServiceConfig.isPublicClient());
|
|
||||||
|
|
||||||
assertTrue("identity-service.use-resource-role-mappings",
|
|
||||||
this.identityServiceConfig.isUseResourceRoleMappings());
|
|
||||||
|
|
||||||
assertTrue("identity-service.enable-cors",
|
|
||||||
this.identityServiceConfig.isCors());
|
|
||||||
|
|
||||||
assertTrue("identity-service.expose-token",
|
|
||||||
this.identityServiceConfig.isExposeToken());
|
|
||||||
|
|
||||||
assertTrue("identity-service.bearer-only",
|
|
||||||
this.identityServiceConfig.isBearerOnly());
|
|
||||||
|
|
||||||
assertTrue("identity-service.autodetect-bearer-only",
|
|
||||||
this.identityServiceConfig.isAutodetectBearerOnly());
|
|
||||||
|
|
||||||
assertTrue("identity-service.enable-basic-auth",
|
|
||||||
this.identityServiceConfig.isEnableBasicAuth());
|
|
||||||
|
|
||||||
assertTrue("identity-service.allow-any-hostname",
|
|
||||||
this.identityServiceConfig.isAllowAnyHostname());
|
|
||||||
|
|
||||||
assertTrue("identity-service.disable-trust-manager",
|
|
||||||
this.identityServiceConfig.isDisableTrustManager());
|
|
||||||
|
|
||||||
assertTrue("identity-service.always-refresh-token",
|
|
||||||
this.identityServiceConfig.isAlwaysRefreshToken());
|
|
||||||
|
|
||||||
assertTrue("identity-service.register-node-at-startup",
|
|
||||||
this.identityServiceConfig.isRegisterNodeAtStartup());
|
|
||||||
|
|
||||||
assertTrue("identity-service.enable-pkce",
|
|
||||||
this.identityServiceConfig.isPkce());
|
|
||||||
|
|
||||||
assertTrue("identity-service.ignore-oauth-query-parameter",
|
|
||||||
this.identityServiceConfig.isIgnoreOAuthQueryParameter());
|
|
||||||
|
|
||||||
assertTrue("identity-service.turn-off-change-session-id-on-login",
|
|
||||||
this.identityServiceConfig.getTurnOffChangeSessionIdOnLogin());
|
|
||||||
|
|
||||||
// check credentials overrides
|
|
||||||
Map<String, Object> credentials = this.identityServiceConfig.getCredentials();
|
|
||||||
assertNotNull("Expected a credentials map", credentials);
|
|
||||||
assertFalse("Expected to retrieve a populated credentials map", credentials.isEmpty());
|
|
||||||
assertEquals("identity-service.credentials.secret", "11111", credentials.get("secret"));
|
|
||||||
assertEquals("identity-service.credentials.provider", "secret", credentials.get("provider"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testValidToken() throws Exception
|
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
|
||||||
{
|
{
|
||||||
// create token
|
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
|
||||||
String jwt = generateToken(false);
|
when(facade.extractUsernameFromToken(anyString()))
|
||||||
|
.thenAnswer(i ->
|
||||||
|
ofNullable(tokenToUser.get(i.getArgument(0, String.class)))
|
||||||
|
.map(Supplier::get));
|
||||||
|
|
||||||
// create mock request object
|
final PersonService personService = mock(PersonService.class);
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
|
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
||||||
|
|
||||||
// validate correct user was found
|
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
|
||||||
assertEquals(TEST_USER_USERNAME, ((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
mapper.setIdentityServiceFacade(facade);
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
mapper.setPersonService(personService);
|
||||||
}
|
mapper.setActive(true);
|
||||||
|
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
|
||||||
|
|
||||||
public void testWrongPublicKey() throws Exception
|
|
||||||
{
|
|
||||||
// generate and apply an incorrect public key
|
|
||||||
childApplicationContextFactory.stop();
|
|
||||||
applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
|
|
||||||
|
|
||||||
// create token
|
return mapper;
|
||||||
String jwt = generateToken(false);
|
|
||||||
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
|
|
||||||
|
|
||||||
// ensure null is returned if the public key is wrong
|
|
||||||
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testWrongPublicKeyWithError() throws Exception
|
|
||||||
{
|
|
||||||
// generate and apply an incorrect public key
|
|
||||||
childApplicationContextFactory.stop();
|
|
||||||
childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
|
|
||||||
applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
|
|
||||||
|
|
||||||
// create token
|
|
||||||
String jwt = generateToken(false);
|
|
||||||
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
|
|
||||||
|
|
||||||
// ensure user mapper falls through instead of throwing an exception
|
|
||||||
String user = ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
|
|
||||||
assertEquals("Returned user should be null when wrong public key is used.", null, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testInvalidJwt() throws Exception
|
|
||||||
{
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest("thisisnotaJWT");
|
|
||||||
|
|
||||||
// ensure null is returned if the JWT is invalid
|
|
||||||
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testMissingToken() throws Exception
|
|
||||||
{
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest("");
|
|
||||||
|
|
||||||
// ensure null is returned if the token is missing
|
|
||||||
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testExpiredToken() throws Exception
|
|
||||||
{
|
|
||||||
// create token
|
|
||||||
String jwt = generateToken(true);
|
|
||||||
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
|
|
||||||
|
|
||||||
// ensure null is returned if the token has expired
|
|
||||||
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testExpiredTokenWithError() throws Exception
|
|
||||||
{
|
|
||||||
// turn on validation failure reporting
|
|
||||||
childApplicationContextFactory.stop();
|
|
||||||
childApplicationContextFactory.setProperty(CONFIG_SILENT_ERRORS, "false");
|
|
||||||
applyHardcodedPublicKey(this.keyPair.getPublic());
|
|
||||||
|
|
||||||
// create token
|
|
||||||
String jwt = generateToken(true);
|
|
||||||
|
|
||||||
// create mock request object
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(jwt);
|
|
||||||
|
|
||||||
// ensure an exception is thrown with correct description
|
|
||||||
String user = ((RemoteUserMapper)childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest);
|
|
||||||
assertEquals("Returned user should be null when the token is expired.", null, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testMissingHeader() throws Exception
|
|
||||||
{
|
|
||||||
// create mock request object with no Authorization header
|
|
||||||
HttpServletRequest mockRequest = createMockTokenRequest(null);
|
|
||||||
|
|
||||||
// ensure null is returned if the header was missing
|
|
||||||
assertNull(((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
|
|
||||||
REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -413,98 +125,11 @@ public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsyste
|
|||||||
authHeaderValues.add(BEARER_PREFIX + token);
|
authHeaderValues.add(BEARER_PREFIX + token);
|
||||||
}
|
}
|
||||||
|
|
||||||
when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements());
|
when(mockRequest.getHeaders(AUTHORIZATION_HEADER))
|
||||||
|
.thenReturn(authHeaderValues.elements());
|
||||||
|
when(mockRequest.getHeader(AUTHORIZATION_HEADER))
|
||||||
|
.thenReturn(authHeaderValues.isEmpty() ? null : authHeaderValues.get(0));
|
||||||
|
|
||||||
return mockRequest;
|
return mockRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method for creating a mocked Servlet request with basic auth.
|
|
||||||
*
|
|
||||||
* @return The mocked request object
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private HttpServletRequest createMockBasicRequest()
|
|
||||||
{
|
|
||||||
// Mock a request with the token in the Authorization header (if supplied)
|
|
||||||
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
|
|
||||||
|
|
||||||
Vector<String> authHeaderValues = new Vector<>(1);
|
|
||||||
String userPwd = TEST_USER_USERNAME + ":" + TEST_USER_USERNAME;
|
|
||||||
authHeaderValues.add(BASIC_PREFIX + Base64.encodeBytes(userPwd.getBytes()));
|
|
||||||
|
|
||||||
// NOTE: as getHeaders gets called twice provide two separate Enumeration objects so that
|
|
||||||
// an empty result is not returned for the second invocation.
|
|
||||||
when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements(),
|
|
||||||
authHeaderValues.elements());
|
|
||||||
|
|
||||||
return mockRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient createMockHttpClient() throws Exception
|
|
||||||
{
|
|
||||||
// mock HttpClient object and set on keycloak deployment to avoid basic auth
|
|
||||||
// attempting to get a token using HTTP POST
|
|
||||||
HttpClient mockHttpClient = mock(HttpClient.class);
|
|
||||||
HttpResponse mockHttpResponse = mock(HttpResponse.class);
|
|
||||||
StatusLine mockStatusLine = mock(StatusLine.class);
|
|
||||||
HttpEntity mockHttpEntity = mock(HttpEntity.class);
|
|
||||||
|
|
||||||
// for the purpose of this test use the same token for access and refresh
|
|
||||||
String token = generateToken(false);
|
|
||||||
String jsonResponse = String.format(PASSWORD_GRANT_RESPONSE, token, token);
|
|
||||||
ByteArrayInputStream jsonResponseStream = new ByteArrayInputStream(jsonResponse.getBytes());
|
|
||||||
|
|
||||||
when(mockHttpClient.execute(any())).thenReturn(mockHttpResponse);
|
|
||||||
when(mockHttpResponse.getStatusLine()).thenReturn(mockStatusLine);
|
|
||||||
when(mockHttpResponse.getEntity()).thenReturn(mockHttpEntity);
|
|
||||||
when(mockStatusLine.getStatusCode()).thenReturn(200);
|
|
||||||
when(mockHttpEntity.getContent()).thenReturn(jsonResponseStream);
|
|
||||||
|
|
||||||
return mockHttpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to create tokens for testing.
|
|
||||||
*
|
|
||||||
* @param expired Determines whether to create an expired JWT
|
|
||||||
* @return The string representation of the JWT
|
|
||||||
*/
|
|
||||||
private String generateToken(boolean expired) throws Exception
|
|
||||||
{
|
|
||||||
String issuerUrl = this.identityServiceConfig.getAuthServerUrl() + "/realms/" + this.identityServiceConfig.getRealm();
|
|
||||||
|
|
||||||
AccessToken token = new AccessToken();
|
|
||||||
token.type("Bearer");
|
|
||||||
token.id("1234");
|
|
||||||
token.subject("abc123");
|
|
||||||
token.issuer(issuerUrl);
|
|
||||||
token.setPreferredUsername(TEST_USER_USERNAME);
|
|
||||||
token.setEmail(TEST_USER_EMAIL);
|
|
||||||
token.setGivenName("Joe");
|
|
||||||
token.setFamilyName("Bloggs");
|
|
||||||
|
|
||||||
if (expired)
|
|
||||||
{
|
|
||||||
token.expiration(Time.currentTime() - 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
String jwt = new JWSBuilder()
|
|
||||||
.jsonContent(token)
|
|
||||||
.rsa256(keyPair.getPrivate());
|
|
||||||
|
|
||||||
return jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the keycloak deployment bean and applies a hardcoded public key locator using the
|
|
||||||
* provided public key.
|
|
||||||
*/
|
|
||||||
private void applyHardcodedPublicKey(PublicKey publicKey)
|
|
||||||
{
|
|
||||||
KeycloakDeployment deployment = (KeycloakDeployment)childApplicationContextFactory.getApplicationContext().
|
|
||||||
getBean(DEPLOYMENT_BEAN_NAME);
|
|
||||||
HardcodedPublicKeyLocator publicKeyLocator = new HardcodedPublicKeyLocator(publicKey);
|
|
||||||
deployment.setPublicKeyLocator(publicKeyLocator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* #%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;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.LazyInstantiatingIdentityServiceFacade;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class LazyInstantiatingIdentityServiceFacadeUnitTest
|
||||||
|
{
|
||||||
|
private static final String USER_NAME = "marlon";
|
||||||
|
private static final String PASSWORD = "brando";
|
||||||
|
private static final String TOKEN = "token";
|
||||||
|
@Test
|
||||||
|
public void shouldRecoverFromInitialAuthorizationServerUnavailability()
|
||||||
|
{
|
||||||
|
final IdentityServiceFacade targetFacade = mock(IdentityServiceFacade.class);
|
||||||
|
final LazyInstantiatingIdentityServiceFacade facade = new LazyInstantiatingIdentityServiceFacade(faultySupplier(3, targetFacade));
|
||||||
|
|
||||||
|
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||||
|
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||||
|
.havingCause().withNoCause().withMessage("Expected failure #1");
|
||||||
|
verifyNoInteractions(targetFacade);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||||
|
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
|
||||||
|
.havingCause().withNoCause().withMessage("Expected failure #2");
|
||||||
|
verifyNoInteractions(targetFacade);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||||
|
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||||
|
.havingCause().withNoCause().withMessage("Expected failure #3");
|
||||||
|
verifyNoInteractions(targetFacade);
|
||||||
|
|
||||||
|
facade.verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
verify(targetFacade).verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldAvoidCreatingMultipleInstanceOfOAuth2AuthorizedClientManager()
|
||||||
|
{
|
||||||
|
final IdentityServiceFacade targetFacade = mock(IdentityServiceFacade.class);
|
||||||
|
final Supplier<IdentityServiceFacade> supplier = mock(Supplier.class);
|
||||||
|
when(supplier.get()).thenReturn(targetFacade);
|
||||||
|
|
||||||
|
final LazyInstantiatingIdentityServiceFacade facade = new LazyInstantiatingIdentityServiceFacade(supplier);
|
||||||
|
|
||||||
|
facade.verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
facade.extractUsernameFromToken(TOKEN);
|
||||||
|
facade.verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
facade.extractUsernameFromToken(TOKEN);
|
||||||
|
facade.verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
verify(supplier, times(1)).get();
|
||||||
|
verify(targetFacade, times(3)).verifyCredentials(USER_NAME, PASSWORD);
|
||||||
|
verify(targetFacade, times(2)).extractUsernameFromToken(TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Supplier<IdentityServiceFacade> faultySupplier(int numberOfInitialFailures, IdentityServiceFacade facade)
|
||||||
|
{
|
||||||
|
final int[] counter = new int[]{0};
|
||||||
|
return () -> {
|
||||||
|
if (counter[0]++ < numberOfInitialFailures)
|
||||||
|
{
|
||||||
|
throw new RuntimeException("Expected failure #" + counter[0]);
|
||||||
|
}
|
||||||
|
return facade;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* #%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;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.SpringBasedIdentityServiceFacade;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
|
||||||
|
public class SpringBasedIdentityServiceFacadeUnitTest
|
||||||
|
{
|
||||||
|
private static final String USER_NAME = "user";
|
||||||
|
private static final String PASSWORD = "password";
|
||||||
|
private static final String TOKEN = "tEsT-tOkEn";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowVerificationExceptionOnFailure()
|
||||||
|
{
|
||||||
|
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
||||||
|
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
||||||
|
when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
|
||||||
|
|
||||||
|
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(CredentialsVerificationException.class)
|
||||||
|
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
|
||||||
|
.havingCause().withNoCause().withMessage("Expected");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowTokenExceptionOnFailure()
|
||||||
|
{
|
||||||
|
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
||||||
|
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
||||||
|
when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
|
||||||
|
|
||||||
|
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(TokenException.class)
|
||||||
|
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||||
|
.havingCause().withNoCause().withMessage("Expected");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* #%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;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.verifyNoInteractions;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
|
|
||||||
import org.alfresco.repo.security.authentication.identityservice.OAuth2ClientFactoryBean.SpringOAuth2Client;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
|
||||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
|
||||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
||||||
|
|
||||||
public class SpringOAuth2ClientUnitTest
|
|
||||||
{
|
|
||||||
private static final String USER_NAME = "user";
|
|
||||||
private static final String PASSWORD = "password";
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void shouldRecoverFromInitialAuthorizationServerUnavailability()
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
|
|
||||||
when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
|
|
||||||
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
|
||||||
when(authClientManager.authorize(any())).thenReturn(authorizedClient);
|
|
||||||
|
|
||||||
final SpringOAuth2Client client = new SpringOAuth2Client(faultySupplier(3, authClientManager));
|
|
||||||
|
|
||||||
assertThatExceptionOfType(CredentialsVerificationException.class)
|
|
||||||
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
|
|
||||||
.havingCause().withNoCause().withMessage("Expected failure #1");
|
|
||||||
verifyNoInteractions(authClientManager);
|
|
||||||
|
|
||||||
assertThatExceptionOfType(CredentialsVerificationException.class)
|
|
||||||
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
|
|
||||||
.havingCause().withNoCause().withMessage("Expected failure #2");
|
|
||||||
verifyNoInteractions(authClientManager);
|
|
||||||
|
|
||||||
assertThatExceptionOfType(CredentialsVerificationException.class)
|
|
||||||
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
|
|
||||||
.havingCause().withNoCause().withMessage("Expected failure #3");
|
|
||||||
verifyNoInteractions(authClientManager);
|
|
||||||
|
|
||||||
client.verifyCredentials(USER_NAME, PASSWORD);
|
|
||||||
verify(authClientManager).authorize(argThat(r -> r.getPrincipal() != null && USER_NAME.equals(r.getPrincipal().getPrincipal())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void shouldThrowVerificationExceptionOnFailure()
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
|
||||||
when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
|
|
||||||
|
|
||||||
final SpringOAuth2Client client = new SpringOAuth2Client(() -> authClientManager);
|
|
||||||
|
|
||||||
assertThatExceptionOfType(CredentialsVerificationException.class)
|
|
||||||
.isThrownBy(() -> client.verifyCredentials(USER_NAME, PASSWORD))
|
|
||||||
.havingCause().withNoCause().withMessage("Expected");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void shouldAvoidCreatingMultipleInstanceOfOAuth2AuthorizedClientManager()
|
|
||||||
{
|
|
||||||
final OAuth2AuthorizedClient authorizedClient = mock(OAuth2AuthorizedClient.class);
|
|
||||||
when(authorizedClient.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
|
|
||||||
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
|
||||||
when(authClientManager.authorize(any())).thenReturn(authorizedClient);
|
|
||||||
final Supplier<OAuth2AuthorizedClientManager> supplier = mock(Supplier.class);
|
|
||||||
when(supplier.get()).thenReturn(authClientManager);
|
|
||||||
|
|
||||||
final SpringOAuth2Client client = new SpringOAuth2Client(supplier);
|
|
||||||
|
|
||||||
client.verifyCredentials(USER_NAME, PASSWORD);
|
|
||||||
client.verifyCredentials(USER_NAME, PASSWORD);
|
|
||||||
client.verifyCredentials(USER_NAME, PASSWORD);
|
|
||||||
verify(supplier, times(1)).get();
|
|
||||||
verify(authClientManager, times(3)).authorize(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Supplier<OAuth2AuthorizedClientManager> faultySupplier(int numberOfInitialFailures, OAuth2AuthorizedClientManager authClientManager)
|
|
||||||
{
|
|
||||||
final int[] counter = new int[]{0};
|
|
||||||
return () -> {
|
|
||||||
if (counter[0]++ < numberOfInitialFailures)
|
|
||||||
{
|
|
||||||
throw new RuntimeException("Expected failure #" + counter[0]);
|
|
||||||
}
|
|
||||||
return authClientManager;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Reference in New Issue
Block a user