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 a8da8a2d28..c22628df93 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,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); } 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 index d669d6053c..0efe457d1d 100644 --- 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 @@ -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 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); + } + } +} \ No newline at end of file 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 index cb5887778d..44a1bf834a 100644 --- 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 @@ -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 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 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 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/IdentityServiceRemoteUserMapper.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/IdentityServiceRemoteUserMapper.java index ab8028fb1f..603aa9f66c 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 @@ -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 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."); diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java new file mode 100644 index 0000000000..c20419cf79 --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java @@ -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 . + * #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 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 createAuthorizationCodeClient(RestOperations rest) + { + final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient(); + client.setRestOperations(rest); + return client; + } + + private static OAuth2AccessTokenResponseClient createRefreshTokenClient(RestOperations rest) + { + final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient(); + client.setRestOperations(rest); + return client; + } + + private static OAuth2AccessTokenResponseClient 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); + } + } +} 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 ad79b82ef8..166647f4cc 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 @@ -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()); 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 eff83bf452..4351828d86 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 @@ -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> 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 usernameSupplier; + + public TestDecodedToken(Supplier 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; + } + } } 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 index 47eb82563e..0c00e51eda 100644 --- 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 @@ -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 faultySupplier(int numberOfInitialFailures, IdentityServiceFacade 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 index 5b3f13531f..1bba1071c6 100644 --- 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 @@ -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(); + } } \ No newline at end of file