diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java
index bee6884e4a..80d17a7471 100644
--- a/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java
+++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/AuthenticationsTest.java
@@ -529,7 +529,7 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
interceptingRemoteUserMapper.setActive(true);
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
- interceptingRemoteUserMapper.setIdentityServiceDeployment(null);
+ interceptingRemoteUserMapper.setIdentityServiceFacade(null);
interceptingRemoteUserMapper.setUserIdToReturn(user2);
remoteUserMapper = interceptingRemoteUserMapper;
}
diff --git a/repository/pom.xml b/repository/pom.xml
index 8c1b59c988..463d4f67a8 100644
--- a/repository/pom.xml
+++ b/repository/pom.xml
@@ -397,6 +397,14 @@
org.springframework.security
spring-security-oauth2-client
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+
org.quartz-scheduler
quartz
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/AlfrescoBearerTokenRequestAuthenticator.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/AlfrescoBearerTokenRequestAuthenticator.java
deleted file mode 100644
index 6c050bc9c4..0000000000
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/AlfrescoBearerTokenRequestAuthenticator.java
+++ /dev/null
@@ -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 .
- * #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);
- }
-}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
index c63b2ff1c2..a8da8a2d28 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponent.java
@@ -28,32 +28,32 @@ package org.alfresco.repo.security.authentication.identityservice;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
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.LogFactory;
/**
*
- * Authenticates a user against Identity Service (Keycloak).
- * {@link OAuth2Client} is used to verify provided user credentials. User is set as the current user if the user
- * credentials are valid.
+ * Authenticates a user against Identity Service (Keycloak/Authorization Server).
+ * {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the
+ * user credentials are valid.
*
- * The {@link IdentityServiceAuthenticationComponent#oAuth2Client} can be null in which case this authenticator will
- * just fall through to the next one in the chain.
+ * The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator
+ * will just fall through to the next one in the chain.
*
*/
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
{
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Authorization Server **/
- private OAuth2Client oAuth2Client;
+ private IdentityServiceFacade identityServiceFacade;
/** enabled flag for the identity service subsystem**/
private boolean active;
private boolean allowGuestLogin;
- public void setOAuth2Client(OAuth2Client oAuth2Client)
+ public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
{
- this.oAuth2Client = oAuth2Client;
+ this.identityServiceFacade = identityServiceFacade;
}
public void setAllowGuestLogin(boolean allowGuestLogin)
@@ -63,21 +63,20 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
{
-
- if (oAuth2Client == null)
+ if (identityServiceFacade == null)
{
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
{
// 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
setCurrentUser(userName);
@@ -108,32 +107,4 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
{
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);
- }
- }
- }
}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceDeploymentFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceDeploymentFactoryBean.java
deleted file mode 100644
index fc26f6f7bc..0000000000
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceDeploymentFactoryBean.java
+++ /dev/null
@@ -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 .
- * #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
-{
- 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 getObjectType()
- {
- return KeycloakDeployment.class;
- }
-
- @Override
- public boolean isSingleton()
- {
- return true;
- }
-}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java
new file mode 100644
index 0000000000..d669d6053c
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacade.java
@@ -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 .
+ * #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 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);
+ }
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java
new file mode 100644
index 0000000000..cb5887778d
--- /dev/null
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceFacadeFactoryBean.java
@@ -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 .
+ * #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}.
+ * This factory can return a null if it is disabled.
+ *
+ */
+public class IdentityServiceFacadeFactoryBean implements FactoryBean
+{
+ 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 targetFacade = new AtomicReference<>();
+ private final Supplier targetFacadeCreator;
+
+ LazyInstantiatingIdentityServiceFacade(Supplier targetFacadeCreator)
+ {
+ this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
+ }
+
+ @Override
+ public void verifyCredentials(String username, String password)
+ {
+ getTargetFacade().verifyCredentials(username, password);
+ }
+
+ @Override
+ public Optional 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 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("typ", "Bearer"::equals),
+ new JwtClaimValidator(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 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 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();
+ }
+ }
+}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceHttpFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceHttpFacade.java
deleted file mode 100644
index aa0e477a2a..0000000000
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceHttpFacade.java
+++ /dev/null
@@ -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 .
- * #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()
- {
- }
- }
-}
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
index ef0c1751bf..ab8028fb1f 100644
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
+++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java
@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
- * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * 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
@@ -27,17 +27,19 @@ package org.alfresco.repo.security.authentication.identityservice;
import javax.servlet.http.HttpServletRequest;
+import java.util.Optional;
+
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.keycloak.adapters.KeycloakDeployment;
-import org.keycloak.adapters.spi.AuthOutcome;
-import org.keycloak.representations.AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
/**
* A {@link RemoteUserMapper} implementation that detects and validates JWTs
@@ -47,7 +49,7 @@ import org.keycloak.representations.AccessToken;
*/
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 */
private boolean isEnabled;
@@ -57,9 +59,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
/** The person service. */
private PersonService personService;
-
- /** The Keycloak deployment object */
- private KeycloakDeployment keycloakDeployment;
+
+ private BearerTokenResolver bearerTokenResolver;
+ private IdentityServiceFacade identityServiceFacade;
/**
* Sets the active flag
@@ -91,58 +93,57 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
{
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)
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(javax.servlet.http.HttpServletRequest)
*/
+ @Override
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
{
- 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);
if (headerUserId != null)
{
// Normalize the user ID taking into account case sensitivity settings
String normalizedUserId = normalizeUserId(headerUserId);
-
- if (logger.isTraceEnabled())
- {
- logger.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
- }
+ LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(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;
}
@@ -163,57 +164,32 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
*/
private String extractUserFromHeader(HttpServletRequest request)
{
- String userName = null;
-
- IdentityServiceHttpFacade facade = new IdentityServiceHttpFacade(request);
-
// 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);
}
-
- AlfrescoBearerTokenRequestAuthenticator tokenAuthenticator =
- new AlfrescoBearerTokenRequestAuthenticator(this.keycloakDeployment);
- AuthOutcome tokenOutcome = tokenAuthenticator.authenticate(facade);
-
- if (logger.isDebugEnabled())
+ catch (OAuth2AuthenticationException e)
{
- logger.debug("Bearer token outcome: " + tokenOutcome);
+ LOGGER.debug("Failed to resolve Bearer token.", e);
+ return null;
}
-
- if (tokenOutcome == AuthOutcome.FAILED && !isValidationFailureSilent)
+
+ final Optional possibleUsername = Optional.ofNullable(bearerToken)
+ .flatMap(identityServiceFacade::extractUsernameFromToken);
+ if (possibleUsername.isEmpty())
{
- throw new AuthenticationException("Token validation failed: " +
- tokenAuthenticator.getValidationFailureDescription());
+ LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
+ return null;
}
-
- if (tokenOutcome == AuthOutcome.AUTHENTICATED)
- {
- 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;
+
+ String username = possibleUsername.get();
+ LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username));
+
+ return username;
}
/**
@@ -238,9 +214,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
}
}, 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;
diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OAuth2ClientFactoryBean.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OAuth2ClientFactoryBean.java
deleted file mode 100644
index bcf01ebaf2..0000000000
--- a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/OAuth2ClientFactoryBean.java
+++ /dev/null
@@ -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 .
- * #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}.
- * The creation of {@link OAuth2Client} requires connection to the Identity Service (Keycloak), disable this factory if
- * the server cannot be reached.
- * This factory can return a null if it is disabled.
- *
- */
-public class OAuth2ClientFactoryBean implements FactoryBean
-{
-
- 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 authorizedClientManagerSupplier;
- private final AtomicReference authorizedClientManager = new AtomicReference<>();
-
- public SpringOAuth2Client(Supplier 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 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;
- }
- }
-}
diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
index e3b5d3e0fc..a467b26824 100644
--- a/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
+++ b/repository/src/main/resources/alfresco/subsystems/Authentication/identity-service/identity-service-authentication-context.xml
@@ -21,12 +21,12 @@
${identity-service.authentication.allowGuestLogin}
-
-
+
+
-
+
@@ -204,12 +204,6 @@
${identity-service.client-socket-timeout:2000}
-
-
-
-
-
-
@@ -222,8 +216,11 @@
-
-
+
+
+
+
+
diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
index 969f70b60e..3b2441f36e 100644
--- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
+++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java
@@ -25,6 +25,8 @@
*/
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.NonBuildTests;
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.util.BeanExtenderUnitTest.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.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
index 009e6ec7df..ad79b82ef8 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceAuthenticationComponentTest.java
@@ -34,8 +34,7 @@ import java.net.ConnectException;
import org.alfresco.error.ExceptionStackUtil;
import org.alfresco.repo.security.authentication.AuthenticationContext;
import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponent.OAuth2Client.CredentialsVerificationException;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.CredentialsVerificationException;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
@@ -65,7 +64,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
- private OAuth2Client mockOAuth2Client;
+ private IdentityServiceFacade mockIdentityServiceFacade;
@Before
public void setUp()
@@ -76,8 +75,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authComponent.setNodeService(nodeService);
authComponent.setPersonService(personService);
- mockOAuth2Client = mock(OAuth2Client.class);
- authComponent.setOAuth2Client(mockOAuth2Client);
+ mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
+ authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
}
@After
@@ -90,7 +89,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
public void testAuthenticationFail()
{
doThrow(new CredentialsVerificationException("Failed"))
- .when(mockOAuth2Client)
+ .when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -100,7 +99,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
public void testAuthenticationFail_connectionException()
{
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
- .when(mockOAuth2Client)
+ .when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
try
@@ -119,7 +118,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
public void testAuthenticationFail_otherException()
{
doThrow(new RuntimeException("Some other errors!"))
- .when(mockOAuth2Client)
+ .when(mockIdentityServiceFacade)
.verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -128,7 +127,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Test
public void testAuthenticationPass()
{
- doNothing().when(mockOAuth2Client).verifyCredentials("username", "password");
+ doNothing().when(mockIdentityServiceFacade).verifyCredentials("username", "password");
authComponent.authenticateImpl("username", "password".toCharArray());
@@ -137,9 +136,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
}
@Test (expected= AuthenticationException.class)
- public void testFallthroughWhenOAuth2ClientIsNull()
+ public void testFallthroughWhenIdentityServiceFacadeIsNull()
{
- authComponent.setOAuth2Client(null);
+ authComponent.setIdentityServiceFacade(null);
authComponent.authenticateImpl("username", "password".toCharArray());
}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
index d669812d0b..eff83bf452 100644
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapperTest.java
@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
- * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * 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
@@ -25,377 +25,89 @@
*/
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.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.Vector;
-import java.util.regex.Pattern;
+import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
-import org.alfresco.repo.management.subsystems.AbstractChainedSubsystemTest;
-import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
-import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
+import junit.framework.TestCase;
import org.alfresco.repo.security.authentication.AuthenticationException;
-import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
-import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
-import org.alfresco.util.ApplicationContextHelper;
-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;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
+import org.alfresco.service.cmr.security.PersonService;
+import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
/**
* Tests the Identity Service based authentication subsystem.
*
* @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 BEARER_PREFIX = "Bearer ";
- private static final String BASIC_PREFIX = "Basic ";
-
- private static final String CONFIG_SILENT_ERRORS = "identity-service.authentication.validation.failure.silent";
-
- 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
+ public void testValidToken()
{
- // switch authentication to use token auth
- childApplicationContextManager = (DefaultChildApplicationContextManager) ctx.getBean("Authentication");
- childApplicationContextManager.stop();
- childApplicationContextManager.setProperty("chain", "identity-service1:identity-service");
- childApplicationContextFactory = getChildApplicationContextFactory(childApplicationContextManager, "identity-service1");
-
- // generate keys for test
- this.keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
-
- // hardcode the realm public key in the deployment bean to stop it fetching keys
- applyHardcodedPublicKey(this.keyPair.getPublic());
-
- // extract config
- this.identityServiceConfig = (IdentityServiceConfig)childApplicationContextFactory.
- getApplicationContext().getBean(CONFIG_BEAN_NAME);
+ final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("VaLiD-ToKeN", () -> "johny"));
+
+ HttpServletRequest mockRequest = createMockTokenRequest("VaLiD-ToKeN");
+
+ final String user = mapper.getRemoteUser(mockRequest);
+ assertEquals("johny", user);
}
- @Override
- protected void tearDown() throws Exception
+ public void testWrongTokenWithSilentValidation()
{
- childApplicationContextManager.destroy();
- childApplicationContextManager = null;
- childApplicationContextFactory = null;
+ final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected ");}));
+ mapper.setValidationFailureSilent(true);
+
+ 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
- String ip = "localhost";
- try {
- Enumeration 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;
+ final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected");}));
+ mapper.setValidationFailureSilent(false);
- Enumeration addresses = iface.getInetAddresses();
- 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);
- }
+ HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
- // check string overrides
- assertEquals("identity-service.auth-server-url", "http://"+ip+":8999/auth",
- this.identityServiceConfig.getAuthServerUrl());
-
- assertEquals("identity-service.realm", "alfresco",
- this.identityServiceConfig.getRealm());
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> mapper.getRemoteUser(mockRequest))
+ .havingCause().withNoCause().withMessage("Expected");
+ }
- 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());
+ private IdentityServiceRemoteUserMapper givenMapper(Map> tokenToUser)
+ {
+ final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
+ when(facade.extractUsernameFromToken(anyString()))
+ .thenAnswer(i ->
+ ofNullable(tokenToUser.get(i.getArgument(0, String.class)))
+ .map(Supplier::get));
- assertEquals("identity-service.client-connection-timeout", 3000,
- this.identityServiceConfig.getClientConnectionTimeout());
+ final PersonService personService = mock(PersonService.class);
+ when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
- assertEquals("identity-service.client-socket-timeout", 1000,
- this.identityServiceConfig.getClientSocketTimeout());
+ final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
+ mapper.setIdentityServiceFacade(facade);
+ mapper.setPersonService(personService);
+ mapper.setActive(true);
+ mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
- // 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 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"));
+ return mapper;
}
-
- public void testValidToken() throws Exception
- {
- // create token
- String jwt = generateToken(false);
-
- // create mock request object
- HttpServletRequest mockRequest = createMockTokenRequest(jwt);
-
- // validate correct user was found
- assertEquals(TEST_USER_USERNAME, ((RemoteUserMapper) childApplicationContextFactory.getApplicationContext().getBean(
- REMOTE_USER_MAPPER_BEAN_NAME)).getRemoteUser(mockRequest));
- }
-
- public void testWrongPublicKey() throws Exception
- {
- // generate and apply an incorrect public key
- childApplicationContextFactory.stop();
- applyHardcodedPublicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
-
- // create token
- 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));
- }
-
+
/**
* Utility method for creating a mocked Servlet request with a token.
*
@@ -412,99 +124,12 @@ public class IdentityServiceRemoteUserMapperTest extends AbstractChainedSubsyste
{
authHeaderValues.add(BEARER_PREFIX + token);
}
-
- when(mockRequest.getHeaders(AUTHORIZATION_HEADER)).thenReturn(authHeaderValues.elements());
-
- 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 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());
+ when(mockRequest.getHeaders(AUTHORIZATION_HEADER))
+ .thenReturn(authHeaderValues.elements());
+ when(mockRequest.getHeader(AUTHORIZATION_HEADER))
+ .thenReturn(authHeaderValues.isEmpty() ? null : authHeaderValues.get(0));
- 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);
+ return mockRequest;
}
}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/LazyInstantiatingIdentityServiceFacadeUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/LazyInstantiatingIdentityServiceFacadeUnitTest.java
new file mode 100644
index 0000000000..47eb82563e
--- /dev/null
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/LazyInstantiatingIdentityServiceFacadeUnitTest.java
@@ -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 .
+ * #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 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 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;
+ };
+ }
+}
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java
new file mode 100644
index 0000000000..5b3f13531f
--- /dev/null
+++ b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java
@@ -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 .
+ * #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");
+ }
+}
\ No newline at end of file
diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringOAuth2ClientUnitTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringOAuth2ClientUnitTest.java
deleted file mode 100644
index 551232d2d3..0000000000
--- a/repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringOAuth2ClientUnitTest.java
+++ /dev/null
@@ -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 .
- * #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 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 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;
- };
- }
-
-}
\ No newline at end of file