mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-31 17:39:05 +00:00
ACS-4847 Expose authorization code and refresh token grant types for the AOS (#1836)
This commit is contained in:
@@ -28,7 +28,8 @@ 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.IdentityServiceFacade.CredentialsVerificationException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
@@ -76,12 +77,12 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
||||
try
|
||||
{
|
||||
// Attempt to verify user credentials
|
||||
identityServiceFacade.verifyCredentials(userName, new String(password));
|
||||
identityServiceFacade.authorize(AuthorizationGrant.password(userName, new String(password)));
|
||||
|
||||
// Verification was successful so treat as authenticated user
|
||||
setCurrentUser(userName);
|
||||
}
|
||||
catch (CredentialsVerificationException e)
|
||||
catch (IdentityServiceFacadeException e)
|
||||
{
|
||||
throw new AuthenticationException("Failed to verify user credentials against the OAuth2 Authorization Server.", e);
|
||||
}
|
||||
|
@@ -25,34 +25,36 @@
|
||||
*/
|
||||
package org.alfresco.repo.security.authentication.identityservice;
|
||||
|
||||
import java.util.Optional;
|
||||
import static java.util.Objects.nonNull;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Allows to interact with the Identity Service
|
||||
*/
|
||||
interface IdentityServiceFacade
|
||||
public 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
|
||||
* Returns {@link AccessToken} based authorization for provided {@link AuthorizationGrant}.
|
||||
* @param grant the OAuth2 grant provided by the Resource Owner.
|
||||
* @return {@link AccessTokenAuthorization} containing access token and optional refresh token.
|
||||
* @throws {@link AuthorizationException} when provided grant cannot be exchanged for the access token.
|
||||
*/
|
||||
void verifyCredentials(String username, String password);
|
||||
AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException;
|
||||
|
||||
/**
|
||||
* Extracts username from provided token
|
||||
*
|
||||
* @param token token representation
|
||||
* @return possible username
|
||||
* Decodes the access token into the {@link DecodedAccessToken} which contains claims connected with a given token.
|
||||
* @param token {@link String} with encoded access token value.
|
||||
* @return {@link DecodedAccessToken} containing decoded claims.
|
||||
* @throws {@link TokenDecodingException} when token decoding failed.
|
||||
*/
|
||||
Optional<String> extractUsernameFromToken(String token);
|
||||
DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
|
||||
|
||||
class IdentityServiceFacadeException extends RuntimeException
|
||||
{
|
||||
IdentityServiceFacadeException(String message)
|
||||
public IdentityServiceFacadeException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
@@ -62,29 +64,149 @@ interface IdentityServiceFacade
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
class CredentialsVerificationException extends IdentityServiceFacadeException
|
||||
|
||||
class AuthorizationException extends IdentityServiceFacadeException
|
||||
{
|
||||
CredentialsVerificationException(String message)
|
||||
AuthorizationException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
CredentialsVerificationException(String message, Throwable cause)
|
||||
AuthorizationException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenException extends IdentityServiceFacadeException
|
||||
class TokenDecodingException extends IdentityServiceFacadeException
|
||||
{
|
||||
TokenException(String message)
|
||||
TokenDecodingException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
TokenException(String message, Throwable cause)
|
||||
TokenDecodingException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents access token authorization with optional refresh token.
|
||||
*/
|
||||
interface AccessTokenAuthorization
|
||||
{
|
||||
/**
|
||||
* Required {@link AccessToken}
|
||||
* @return {@link AccessToken}
|
||||
*/
|
||||
AccessToken getAccessToken();
|
||||
|
||||
/**
|
||||
* Optional refresh token.
|
||||
* @return Refresh token or {@code null}
|
||||
*/
|
||||
String getRefreshTokenValue();
|
||||
}
|
||||
|
||||
interface AccessToken {
|
||||
String getTokenValue();
|
||||
Instant getExpiresAt();
|
||||
}
|
||||
|
||||
interface DecodedAccessToken extends AccessToken
|
||||
{
|
||||
Object getClaim(String claim);
|
||||
}
|
||||
|
||||
class AuthorizationGrant {
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String refreshToken;
|
||||
private final String authorizationCode;
|
||||
private final String redirectUri;
|
||||
|
||||
private AuthorizationGrant(String username, String password, String refreshToken, String authorizationCode, String redirectUri)
|
||||
{
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.refreshToken = refreshToken;
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public static AuthorizationGrant password(String username, String password)
|
||||
{
|
||||
return new AuthorizationGrant(requireNonNull(username), requireNonNull(password), null, null, null);
|
||||
}
|
||||
|
||||
public static AuthorizationGrant refreshToken(String refreshToken)
|
||||
{
|
||||
return new AuthorizationGrant(null, null, requireNonNull(refreshToken), null, null);
|
||||
}
|
||||
|
||||
public static AuthorizationGrant authorizationCode(String authorizationCode, String redirectUri)
|
||||
{
|
||||
return new AuthorizationGrant(null, null, null, requireNonNull(authorizationCode), requireNonNull(redirectUri));
|
||||
}
|
||||
|
||||
boolean isPassword()
|
||||
{
|
||||
return nonNull(username);
|
||||
}
|
||||
|
||||
boolean isRefreshToken()
|
||||
{
|
||||
return nonNull(refreshToken);
|
||||
}
|
||||
|
||||
boolean isAuthorizationCode()
|
||||
{
|
||||
return nonNull(authorizationCode);
|
||||
}
|
||||
|
||||
String getUsername()
|
||||
{
|
||||
return username;
|
||||
}
|
||||
|
||||
String getPassword()
|
||||
{
|
||||
return password;
|
||||
}
|
||||
|
||||
String getRefreshToken()
|
||||
{
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
String getAuthorizationCode()
|
||||
{
|
||||
return authorizationCode;
|
||||
}
|
||||
|
||||
String getRedirectUri()
|
||||
{
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AuthorizationGrant that = (AuthorizationGrant) o;
|
||||
return Objects.equals(username, that.username) &&
|
||||
Objects.equals(password, that.password) &&
|
||||
Objects.equals(refreshToken, that.refreshToken) &&
|
||||
Objects.equals(authorizationCode, that.authorizationCode) &&
|
||||
Objects.equals(redirectUri, that.redirectUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(username, password, refreshToken, authorizationCode, redirectUri);
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,9 +32,7 @@ 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;
|
||||
@@ -43,28 +41,14 @@ 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;
|
||||
@@ -137,15 +121,15 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyCredentials(String username, String password)
|
||||
public AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException
|
||||
{
|
||||
getTargetFacade().verifyCredentials(username, password);
|
||||
return getTargetFacade().authorize(grant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> extractUsernameFromToken(String token)
|
||||
public DecodedAccessToken decodeToken(String token) throws TokenDecodingException
|
||||
{
|
||||
return getTargetFacade().extractUsernameFromToken(token);
|
||||
return getTargetFacade().decodeToken(token);
|
||||
}
|
||||
|
||||
private IdentityServiceFacade getTargetFacade()
|
||||
@@ -188,15 +172,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
//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 ClientRegistration clientRegistration = createClientRegistration();
|
||||
final JwtDecoder jwtDecoder = createJwtDecoder(clientRegistration);
|
||||
|
||||
return new SpringBasedIdentityServiceFacade(clientManager, jwtDecoder);
|
||||
return new SpringBasedIdentityServiceFacade(restTemplate, clientRegistration, jwtDecoder);
|
||||
}
|
||||
|
||||
private RestTemplate createRestTemplate()
|
||||
@@ -212,7 +193,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
private ClientRegistration createClientRegistration(RestTemplate restTemplate)
|
||||
private ClientRegistration createClientRegistration()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -222,7 +203,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
.clientSecret(config.getClientSecret())
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.registrationId(SpringBasedIdentityServiceFacade.CLIENT_REGISTRATION_ID)
|
||||
.registrationId("ids")
|
||||
.build();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
@@ -232,28 +213,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientManager createAuthorizedClientManager(RestTemplate restTemplate, ClientRegistration clientRegistration)
|
||||
{
|
||||
final AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
|
||||
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
|
||||
new SingleClientRegistration(clientRegistration),
|
||||
new NoStoredAuthorizedClient());
|
||||
|
||||
final Consumer<PasswordGrantBuilder> passwordGrantConfigurer = b -> {
|
||||
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
||||
client.setRestOperations(restTemplate);
|
||||
b.accessTokenResponseClient(client);
|
||||
|
||||
b.clockSkew(Duration.of(CLOCK_SKEW_MS, ChronoUnit.MILLIS));
|
||||
};
|
||||
manager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder()
|
||||
.password(passwordGrantConfigurer)
|
||||
.build());
|
||||
manager.setContextAttributesMapper(OAuth2AuthorizeRequest::getAttributes);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
private JwtDecoder createJwtDecoder(ClientRegistration clientRegistration)
|
||||
{
|
||||
final OidcIdTokenDecoderFactory decoderFactory = new OidcIdTokenDecoderFactory();
|
||||
@@ -274,115 +233,5 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
throw authorizationServerCantBeUsedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoStoredAuthorizedClient implements OAuth2AuthorizedClientService
|
||||
{
|
||||
|
||||
@Override
|
||||
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAuthorizedClient(String clientRegistrationId, String principalName)
|
||||
{
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleClientRegistration implements ClientRegistrationRepository
|
||||
{
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
private SingleClientRegistration(ClientRegistration clientRegistration)
|
||||
{
|
||||
this.clientRegistration = requireNonNull(clientRegistration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRegistration findByRegistrationId(String registrationId)
|
||||
{
|
||||
return Objects.equals(registrationId, clientRegistration.getRegistrationId()) ? clientRegistration : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||
{
|
||||
static final String CLIENT_REGISTRATION_ID = "ids";
|
||||
private final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;
|
||||
private JwtDecoder jwtDecoder;
|
||||
|
||||
SpringBasedIdentityServiceFacade(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, JwtDecoder jwtDecoder)
|
||||
{
|
||||
this.oAuth2AuthorizedClientManager = requireNonNull(oAuth2AuthorizedClientManager);
|
||||
this.jwtDecoder = requireNonNull(jwtDecoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyCredentials(String username, String password)
|
||||
{
|
||||
final OAuth2AuthorizedClient authorizedClient;
|
||||
try
|
||||
{
|
||||
final OAuth2AuthorizeRequest authRequest = createPasswordCredentialsRequest(username, password);
|
||||
authorizedClient = oAuth2AuthorizedClientManager.authorize(authRequest);
|
||||
}
|
||||
catch (OAuth2AuthorizationException e)
|
||||
{
|
||||
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
|
||||
throw new CredentialsVerificationException("Authorization against the Authorization Server failed with " + e.getError() + ".", e);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
|
||||
throw new CredentialsVerificationException("Failed to authorize against Authorization Server.", e);
|
||||
}
|
||||
|
||||
if (authorizedClient == null || authorizedClient.getAccessToken() == null)
|
||||
{
|
||||
throw new CredentialsVerificationException("Resource Owner Password Credentials is not supported by the Authorization Server.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> extractUsernameFromToken(String token)
|
||||
{
|
||||
final Jwt validToken;
|
||||
try
|
||||
{
|
||||
validToken = jwtDecoder.decode(requireNonNull(token));
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new TokenException("Failed to decode token. " + e.getMessage(), e);
|
||||
}
|
||||
if (LOGGER.isDebugEnabled())
|
||||
{
|
||||
LOGGER.debug("Bearer token outcome: " + validToken);
|
||||
}
|
||||
return Optional.ofNullable(validToken)
|
||||
.map(Jwt::getClaims)
|
||||
.map(c -> c.get("preferred_username"))
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizeRequest createPasswordCredentialsRequest(String userName, String password)
|
||||
{
|
||||
return OAuth2AuthorizeRequest
|
||||
.withClientRegistrationId(CLIENT_REGISTRATION_ID)
|
||||
.principal(userName)
|
||||
.attribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, userName)
|
||||
.attribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ 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.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -50,6 +50,7 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv
|
||||
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
|
||||
{
|
||||
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
|
||||
static final String USERNAME_CLAIM = "preferred_username";
|
||||
|
||||
/** Is the mapper enabled */
|
||||
private boolean isEnabled;
|
||||
@@ -131,7 +132,7 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
||||
return normalizedUserId;
|
||||
}
|
||||
}
|
||||
catch (TokenException e)
|
||||
catch (IdentityServiceFacadeException e)
|
||||
{
|
||||
if (!isValidationFailureSilent)
|
||||
{
|
||||
@@ -160,7 +161,7 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
||||
* Extracts the user name from the JWT in the given request.
|
||||
*
|
||||
* @param request The request containing the JWT
|
||||
* @return The user name or null if it can not be determined
|
||||
* @return The username or null if it can not be determined
|
||||
*/
|
||||
private String extractUserFromHeader(HttpServletRequest request)
|
||||
{
|
||||
@@ -179,7 +180,11 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
||||
}
|
||||
|
||||
final Optional<String> possibleUsername = Optional.ofNullable(bearerToken)
|
||||
.flatMap(identityServiceFacade::extractUsernameFromToken);
|
||||
.map(identityServiceFacade::decodeToken)
|
||||
.map(t -> t.getClaim(USERNAME_CLAIM))
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast);
|
||||
|
||||
if (possibleUsername.isEmpty())
|
||||
{
|
||||
LOGGER.debug("User could not be authenticated by IdentityServiceRemoteUserMapper.");
|
||||
|
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* Alfresco is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Alfresco is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.repo.security.authentication.identityservice;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||
{
|
||||
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);
|
||||
private static final Instant SOME_INSIGNIFICANT_DATE_IN_THE_PAST = Instant.MIN.plusSeconds(12345);
|
||||
private final Map<AuthorizationGrantType, OAuth2AccessTokenResponseClient> clients;
|
||||
private final ClientRegistration clientRegistration;
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
SpringBasedIdentityServiceFacade(RestOperations restOperations, ClientRegistration clientRegistration, JwtDecoder jwtDecoder)
|
||||
{
|
||||
requireNonNull(restOperations);
|
||||
this.clientRegistration = requireNonNull(clientRegistration);
|
||||
this.jwtDecoder = requireNonNull(jwtDecoder);
|
||||
this.clients = Map.of(
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
|
||||
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
|
||||
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenAuthorization authorize(AuthorizationGrant authorizationGrant)
|
||||
{
|
||||
final AbstractOAuth2AuthorizationGrantRequest request = createRequest(authorizationGrant);
|
||||
final OAuth2AccessTokenResponseClient client = getClient(request);
|
||||
|
||||
final OAuth2AccessTokenResponse response;
|
||||
try
|
||||
{
|
||||
response = client.getTokenResponse(request);
|
||||
}
|
||||
catch (OAuth2AuthorizationException e)
|
||||
{
|
||||
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");
|
||||
throw new AuthorizationException("Failed to obtain access token. " + e.getError(), e);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
LOGGER.warn("Failed to authorize against Authorization Server. Reason: " + e.getMessage());
|
||||
throw new AuthorizationException("Failed to obtain access token.", e);
|
||||
}
|
||||
|
||||
return new SpringAccessTokenAuthorization(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecodedAccessToken decodeToken(String token)
|
||||
{
|
||||
final Jwt validToken;
|
||||
try
|
||||
{
|
||||
validToken = jwtDecoder.decode(token);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new TokenDecodingException("Failed to decode token. " + e.getMessage(), e);
|
||||
}
|
||||
if (LOGGER.isDebugEnabled())
|
||||
{
|
||||
LOGGER.debug("Bearer token outcome: " + validToken.getClaims());
|
||||
}
|
||||
return new SpringDecodedAccessToken(validToken);
|
||||
}
|
||||
|
||||
private AbstractOAuth2AuthorizationGrantRequest createRequest(AuthorizationGrant grant)
|
||||
{
|
||||
if (grant.isPassword())
|
||||
{
|
||||
return new OAuth2PasswordGrantRequest(clientRegistration, grant.getUsername(), grant.getPassword());
|
||||
}
|
||||
|
||||
if (grant.isRefreshToken())
|
||||
{
|
||||
final OAuth2AccessToken expiredAccessToken = new OAuth2AccessToken(
|
||||
TokenType.BEARER,
|
||||
"JUST_FOR_FULFILLING_THE_SPRING_API",
|
||||
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
|
||||
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
|
||||
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
|
||||
|
||||
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken, clientRegistration.getScopes());
|
||||
}
|
||||
|
||||
if (grant.isAuthorizationCode())
|
||||
{
|
||||
final OAuth2AuthorizationExchange authzExchange = new OAuth2AuthorizationExchange(
|
||||
OAuth2AuthorizationRequest.authorizationCode()
|
||||
.clientId(clientRegistration.getClientId())
|
||||
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||
.redirectUri(grant.getRedirectUri())
|
||||
.scopes(clientRegistration.getScopes())
|
||||
.build(),
|
||||
OAuth2AuthorizationResponse.success(grant.getAuthorizationCode())
|
||||
.redirectUri(grant.getRedirectUri())
|
||||
.build()
|
||||
);
|
||||
return new OAuth2AuthorizationCodeGrantRequest(clientRegistration, authzExchange);
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Unsupported grant type.");
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponseClient getClient(AbstractOAuth2AuthorizationGrantRequest request)
|
||||
{
|
||||
final AuthorizationGrantType grantType = request.getGrantType();
|
||||
final OAuth2AccessTokenResponseClient client = clients.get(grantType);
|
||||
if (client == null)
|
||||
{
|
||||
throw new UnsupportedOperationException("Unsupported grant type `" + grantType + "`.");
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createAuthorizationCodeClient(RestOperations rest)
|
||||
{
|
||||
final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
|
||||
client.setRestOperations(rest);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> createRefreshTokenClient(RestOperations rest)
|
||||
{
|
||||
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
|
||||
client.setRestOperations(rest);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest)
|
||||
{
|
||||
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
|
||||
client.setRestOperations(rest);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
|
||||
{
|
||||
private final OAuth2AccessTokenResponse tokenResponse;
|
||||
|
||||
private SpringAccessTokenAuthorization(OAuth2AccessTokenResponse tokenResponse)
|
||||
{
|
||||
this.tokenResponse = requireNonNull(tokenResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessToken getAccessToken()
|
||||
{
|
||||
return new SpringAccessToken(tokenResponse.getAccessToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRefreshTokenValue()
|
||||
{
|
||||
return Optional.of(tokenResponse)
|
||||
.map(OAuth2AccessTokenResponse::getRefreshToken)
|
||||
.map(AbstractOAuth2Token::getTokenValue)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpringAccessToken implements AccessToken
|
||||
{
|
||||
private final AbstractOAuth2Token token;
|
||||
|
||||
private SpringAccessToken(AbstractOAuth2Token token)
|
||||
{
|
||||
this.token = requireNonNull(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenValue()
|
||||
{
|
||||
return token.getTokenValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiresAt()
|
||||
{
|
||||
return token.getExpiresAt();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpringDecodedAccessToken extends SpringAccessToken implements DecodedAccessToken
|
||||
{
|
||||
private final Jwt jwt;
|
||||
|
||||
private SpringDecodedAccessToken(Jwt jwt)
|
||||
{
|
||||
super(jwt);
|
||||
this.jwt = jwt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getClaim(String claim)
|
||||
{
|
||||
return jwt.getClaim(claim);
|
||||
}
|
||||
}
|
||||
}
|
@@ -25,16 +25,18 @@
|
||||
*/
|
||||
package org.alfresco.repo.security.authentication.identityservice;
|
||||
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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.IdentityServiceFacade.CredentialsVerificationException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
||||
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
|
||||
import org.alfresco.service.cmr.repository.NodeService;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
@@ -88,9 +90,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
||||
@Test (expected=AuthenticationException.class)
|
||||
public void testAuthenticationFail()
|
||||
{
|
||||
doThrow(new CredentialsVerificationException("Failed"))
|
||||
.when(mockIdentityServiceFacade)
|
||||
.verifyCredentials("username", "password");
|
||||
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
||||
|
||||
doThrow(new AuthorizationException("Failed")).when(mockIdentityServiceFacade).authorize(grant);
|
||||
|
||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||
}
|
||||
@@ -98,9 +100,10 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
||||
@Test(expected = AuthenticationException.class)
|
||||
public void testAuthenticationFail_connectionException()
|
||||
{
|
||||
doThrow(new CredentialsVerificationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
|
||||
.when(mockIdentityServiceFacade)
|
||||
.verifyCredentials("username", "password");
|
||||
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
||||
|
||||
doThrow(new AuthorizationException("Couldn't connect to server", new ConnectException("ConnectionRefused")))
|
||||
.when(mockIdentityServiceFacade).authorize(grant);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -117,9 +120,11 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
||||
@Test (expected=AuthenticationException.class)
|
||||
public void testAuthenticationFail_otherException()
|
||||
{
|
||||
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
||||
|
||||
doThrow(new RuntimeException("Some other errors!"))
|
||||
.when(mockIdentityServiceFacade)
|
||||
.verifyCredentials("username", "password");
|
||||
.authorize(grant);
|
||||
|
||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||
}
|
||||
@@ -127,7 +132,10 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
||||
@Test
|
||||
public void testAuthenticationPass()
|
||||
{
|
||||
doNothing().when(mockIdentityServiceFacade).verifyCredentials("username", "password");
|
||||
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
||||
AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
|
||||
|
||||
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
|
||||
|
||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||
|
||||
|
@@ -27,11 +27,13 @@ package org.alfresco.repo.security.authentication.identityservice;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
|
||||
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.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.function.Supplier;
|
||||
@@ -40,7 +42,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
|
||||
import org.alfresco.service.cmr.security.PersonService;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
|
||||
@@ -66,7 +69,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
||||
|
||||
public void testWrongTokenWithSilentValidation()
|
||||
{
|
||||
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected ");}));
|
||||
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected ");}));
|
||||
mapper.setValidationFailureSilent(true);
|
||||
|
||||
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
|
||||
@@ -77,7 +80,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
||||
|
||||
public void testWrongTokenWithoutSilentValidation()
|
||||
{
|
||||
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenException("Expected");}));
|
||||
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected");}));
|
||||
mapper.setValidationFailureSilent(false);
|
||||
|
||||
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
|
||||
@@ -90,10 +93,8 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
||||
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
|
||||
{
|
||||
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
|
||||
when(facade.extractUsernameFromToken(anyString()))
|
||||
.thenAnswer(i ->
|
||||
ofNullable(tokenToUser.get(i.getArgument(0, String.class)))
|
||||
.map(Supplier::get));
|
||||
when(facade.decodeToken(anyString()))
|
||||
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
|
||||
|
||||
final PersonService personService = mock(PersonService.class);
|
||||
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
||||
@@ -132,4 +133,34 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
||||
|
||||
return mockRequest;
|
||||
}
|
||||
|
||||
private static class TestDecodedToken implements DecodedAccessToken
|
||||
{
|
||||
|
||||
private final Supplier<String> usernameSupplier;
|
||||
|
||||
public TestDecodedToken(Supplier<String> usernameSupplier)
|
||||
{
|
||||
|
||||
this.usernameSupplier = usernameSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenValue()
|
||||
{
|
||||
return "TEST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiresAt()
|
||||
{
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getClaim(String claim)
|
||||
{
|
||||
return USERNAME_CLAIM.equals(claim) ? usernameSupplier.get() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,6 +34,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.LazyInstantiatingIdentityServiceFacade;
|
||||
import org.junit.Test;
|
||||
@@ -50,22 +51,23 @@ public class LazyInstantiatingIdentityServiceFacadeUnitTest
|
||||
final LazyInstantiatingIdentityServiceFacade facade = new LazyInstantiatingIdentityServiceFacade(faultySupplier(3, targetFacade));
|
||||
|
||||
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||
.isThrownBy(() -> facade.decodeToken(TOKEN))
|
||||
.havingCause().withNoCause().withMessage("Expected failure #1");
|
||||
verifyNoInteractions(targetFacade);
|
||||
|
||||
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
|
||||
.isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
|
||||
.havingCause().withNoCause().withMessage("Expected failure #2");
|
||||
verifyNoInteractions(targetFacade);
|
||||
|
||||
assertThatExceptionOfType(IdentityServiceFacadeException.class)
|
||||
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||
.isThrownBy(() -> facade.decodeToken(TOKEN))
|
||||
.havingCause().withNoCause().withMessage("Expected failure #3");
|
||||
verifyNoInteractions(targetFacade);
|
||||
|
||||
facade.verifyCredentials(USER_NAME, PASSWORD);
|
||||
verify(targetFacade).verifyCredentials(USER_NAME, PASSWORD);
|
||||
final AuthorizationGrant grant = AuthorizationGrant.password(USER_NAME, PASSWORD);
|
||||
facade.authorize(grant);
|
||||
verify(targetFacade).authorize(grant);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,14 +79,14 @@ public class LazyInstantiatingIdentityServiceFacadeUnitTest
|
||||
|
||||
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);
|
||||
facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
|
||||
facade.decodeToken(TOKEN);
|
||||
facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
|
||||
facade.decodeToken(TOKEN);
|
||||
facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
|
||||
verify(supplier, times(1)).get();
|
||||
verify(targetFacade, times(3)).verifyCredentials(USER_NAME, PASSWORD);
|
||||
verify(targetFacade, times(2)).extractUsernameFromToken(TOKEN);
|
||||
verify(targetFacade, times(3)).authorize(AuthorizationGrant.password(USER_NAME, PASSWORD));
|
||||
verify(targetFacade, times(2)).decodeToken(TOKEN);
|
||||
}
|
||||
|
||||
private Supplier<IdentityServiceFacade> faultySupplier(int numberOfInitialFailures, IdentityServiceFacade facade)
|
||||
|
@@ -30,12 +30,14 @@ 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.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationException;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AuthorizationGrant;
|
||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
public class SpringBasedIdentityServiceFacadeUnitTest
|
||||
{
|
||||
@@ -46,28 +48,37 @@ public class SpringBasedIdentityServiceFacadeUnitTest
|
||||
@Test
|
||||
public void shouldThrowVerificationExceptionOnFailure()
|
||||
{
|
||||
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
||||
final RestOperations restOperations = mock(RestOperations.class);
|
||||
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
||||
when(authClientManager.authorize(any())).thenThrow(new RuntimeException("Expected"));
|
||||
when(restOperations.exchange(any(), any(Class.class))).thenThrow(new RuntimeException("Expected"));
|
||||
|
||||
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
|
||||
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
|
||||
|
||||
assertThatExceptionOfType(CredentialsVerificationException.class)
|
||||
.isThrownBy(() -> facade.verifyCredentials(USER_NAME, PASSWORD))
|
||||
assertThatExceptionOfType(AuthorizationException.class)
|
||||
.isThrownBy(() -> facade.authorize(AuthorizationGrant.password(USER_NAME, PASSWORD)))
|
||||
.havingCause().withNoCause().withMessage("Expected");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldThrowTokenExceptionOnFailure()
|
||||
{
|
||||
final OAuth2AuthorizedClientManager authClientManager = mock(OAuth2AuthorizedClientManager.class);
|
||||
final RestOperations restOperations = mock(RestOperations.class);
|
||||
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
||||
when(jwtDecoder.decode(TOKEN)).thenThrow(new RuntimeException("Expected"));
|
||||
|
||||
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(authClientManager, jwtDecoder);
|
||||
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
|
||||
|
||||
assertThatExceptionOfType(TokenException.class)
|
||||
.isThrownBy(() -> facade.extractUsernameFromToken(TOKEN))
|
||||
assertThatExceptionOfType(TokenDecodingException.class)
|
||||
.isThrownBy(() -> facade.decodeToken(TOKEN))
|
||||
.havingCause().withNoCause().withMessage("Expected");
|
||||
}
|
||||
|
||||
private ClientRegistration testRegistration()
|
||||
{
|
||||
return ClientRegistration.withRegistrationId("test")
|
||||
.tokenUri("http://localhost")
|
||||
.clientId("test")
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.build();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user