ACS-4847 Expose authorization code and refresh token grant types for the AOS (#1836)

This commit is contained in:
Piotr Żurek
2023-03-29 14:16:08 +02:00
committed by GitHub
parent 73a1de37f6
commit 82df7ce5d4
9 changed files with 514 additions and 230 deletions

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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.");

View File

@@ -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);
}
}
}

View File

@@ -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());

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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();
}
}