ACS-9416 Backport ACS-9414 Enhance the Identity Provider configuration (#3263) (#3341)

This commit is contained in:
Damian Ujma
2025-05-14 14:00:09 +02:00
committed by Gerard Olenski
parent be2860ffdd
commit 7be9ce394d
28 changed files with 3595 additions and 2826 deletions

View File

@@ -145,7 +145,7 @@ jobs:
- uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/free-hosted-runner-disk-space@v8.16.0
- uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v8.16.0
- uses: Alfresco/ya-pmd-scan@v4.1.0
- uses: Alfresco/ya-pmd-scan@v4.3.0
with:
classpath-build-command: "mvn test-compile -ntp -Pags -pl \"-:alfresco-community-repo-docker\""

View File

@@ -1627,7 +1627,7 @@
"filename": "repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 46,
"line_number": 48,
"is_secret": false
}
],
@@ -1888,5 +1888,5 @@
}
]
},
"generated_at": "2024-10-09T09:32:52Z"
"generated_at": "2025-05-13T10:16:00Z"
}

View File

@@ -25,22 +25,20 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.AuthorizationGrant;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
/**
*
* Authenticates a user against Identity Service (Keycloak/Authorization Server).
* {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the
* user credentials are valid.
* <br>
* The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator
* will just fall through to the next one in the chain.
* Authenticates a user against Identity Service (Keycloak/Authorization Server). {@link IdentityServiceFacade} is used to verify provided user credentials. User is set as the current user if the user credentials are valid. <br>
* The {@link IdentityServiceAuthenticationComponent#identityServiceFacade} can be null in which case this authenticator will just fall through to the next one in the chain.
*
*/
public class IdentityServiceAuthenticationComponent extends AbstractAuthenticationComponent implements ActivateableBean
@@ -48,7 +46,7 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
private final Log LOGGER = LogFactory.getLog(IdentityServiceAuthenticationComponent.class);
/** client used to authenticate user credentials against Authorization Server **/
private IdentityServiceFacade identityServiceFacade;
/** enabled flag for the identity service subsystem**/
/** enabled flag for the identity service subsystem **/
private boolean active;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
@@ -89,8 +87,8 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(AuthorizationGrant.password(userName, String.valueOf(password)));
String normalizedUsername = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(accessTokenAuthorization.getAccessToken().getTokenValue())
.map(OIDCUserInfo::username)
.orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint."));
.map(OIDCUserInfo::username)
.orElseThrow(() -> new AuthenticationException("Failed to extract username from token and user info endpoint."));
// Verification was successful so treat as authenticated user
setCurrentUser(normalizedUsername);
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -69,6 +69,13 @@ public class IdentityServiceConfig
private boolean clientIdValidationDisabled;
private String adminConsoleRedirectPath;
private String signatureAlgorithms;
private String adminConsoleScopes;
private String passwordGrantScopes;
private String issuerAttribute;
private String firstNameAttribute;
private String lastNameAttribute;
private String emailAttribute;
private long jwtClockSkewMs;
/**
*
@@ -81,7 +88,8 @@ public class IdentityServiceConfig
/**
*
* @param clientConnectionTimeout Client connection timeout in milliseconds.
* @param clientConnectionTimeout
* Client connection timeout in milliseconds.
*/
public void setClientConnectionTimeout(int clientConnectionTimeout)
{
@@ -99,7 +107,8 @@ public class IdentityServiceConfig
/**
*
* @param clientSocketTimeout Client socket timeout in milliseconds.
* @param clientSocketTimeout
* Client socket timeout in milliseconds.
*/
public void setClientSocketTimeout(int clientSocketTimeout)
{
@@ -139,13 +148,13 @@ public class IdentityServiceConfig
public String getAuthServerUrl()
{
return Optional.ofNullable(realm)
.filter(StringUtils::isNotBlank)
.filter(realm -> StringUtils.isNotBlank(authServerUrl))
.map(realm -> UriComponentsBuilder.fromUriString(authServerUrl)
.pathSegment(REALMS, realm)
.build()
.toString())
.orElse(authServerUrl);
.filter(StringUtils::isNotBlank)
.filter(realm -> StringUtils.isNotBlank(authServerUrl))
.map(realm -> UriComponentsBuilder.fromUriString(authServerUrl)
.pathSegment(REALMS, realm)
.build()
.toString())
.orElse(authServerUrl);
}
public void setAuthServerUrl(String authServerUrl)
@@ -181,7 +190,7 @@ public class IdentityServiceConfig
public String getClientSecret()
{
return Optional.ofNullable(clientSecret)
.orElse("");
.orElse("");
}
public void setAllowAnyHostname(boolean allowAnyHostname)
@@ -317,14 +326,88 @@ public class IdentityServiceConfig
public Set<SignatureAlgorithm> getSignatureAlgorithms()
{
return Stream.of(signatureAlgorithms.split(","))
.map(String::trim)
.map(SignatureAlgorithm::from)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableSet());
.map(String::trim)
.map(SignatureAlgorithm::from)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableSet());
}
public void setSignatureAlgorithms(String signatureAlgorithms)
{
this.signatureAlgorithms = signatureAlgorithms;
}
public String getIssuerAttribute()
{
return issuerAttribute;
}
public void setIssuerAttribute(String issuerAttribute)
{
this.issuerAttribute = issuerAttribute;
}
public Set<String> getAdminConsoleScopes()
{
return Stream.of(adminConsoleScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setAdminConsoleScopes(String adminConsoleScopes)
{
this.adminConsoleScopes = adminConsoleScopes;
}
public Set<String> getPasswordGrantScopes()
{
return Stream.of(passwordGrantScopes.split(","))
.map(String::trim)
.collect(Collectors.toUnmodifiableSet());
}
public void setPasswordGrantScopes(String passwordGrantScopes)
{
this.passwordGrantScopes = passwordGrantScopes;
}
public void setFirstNameAttribute(String firstNameAttribute)
{
this.firstNameAttribute = firstNameAttribute;
}
public void setLastNameAttribute(String lastNameAttribute)
{
this.lastNameAttribute = lastNameAttribute;
}
public void setEmailAttribute(String emailAttribute)
{
this.emailAttribute = emailAttribute;
}
public void setJwtClockSkewMs(long jwtClockSkewMs)
{
this.jwtClockSkewMs = jwtClockSkewMs;
}
public String getFirstNameAttribute()
{
return firstNameAttribute;
}
public String getLastNameAttribute()
{
return lastNameAttribute;
}
public String getEmailAttribute()
{
return emailAttribute;
}
public long getJwtClockSkewMs()
{
return jwtClockSkewMs;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -34,6 +34,9 @@ import java.util.Optional;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Allows to interact with the Identity Service
*/
@@ -41,28 +44,36 @@ public interface IdentityServiceFacade
{
/**
* Returns {@link AccessToken} based authorization for provided {@link AuthorizationGrant}.
* @param grant the OAuth2 grant provided by the Resource Owner.
*
* @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.
* @throws {@link
* AuthorizationException} when provided grant cannot be exchanged for the access token.
*/
AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException;
/**
* 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.
*
* @param token
* {@link String} with encoded access token value.
* @return {@link DecodedAccessToken} containing decoded claims.
* @throws {@link TokenDecodingException} when token decoding failed.
* @throws {@link
* TokenDecodingException} when token decoding failed.
*/
DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
/**
* Gets claims about the authenticated user,
* such as name and email address, via the UserInfo endpoint of the OpenID provider.
* @param token {@link String} with encoded access token value.
* @param principalAttribute {@link String} the attribute name used to access the user's name from the user info response.
* @return {@link OIDCUserInfo} containing user claims.
* Gets claims about the authenticated user, such as name and email address, via the UserInfo endpoint of the OpenID provider.
*
* @param token
* {@link String} with encoded access token value.
* @param userInfoAttrMapping
* {@link UserInfoAttrMapping} containing the mapping of claims.
* @return {@link DecodedTokenUser} containing user claims or {@link Optional#empty()} if the token does not contain a username claim.
*/
Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute);
Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping);
/**
* Gets a client registration
@@ -129,19 +140,23 @@ public interface IdentityServiceFacade
{
/**
* Required {@link AccessToken}
*
* @return {@link AccessToken}
*/
AccessToken getAccessToken();
/**
* Optional refresh token.
*
* @return Refresh token or {@code null}
*/
String getRefreshTokenValue();
}
interface AccessToken {
interface AccessToken
{
String getTokenValue();
Instant getExpiresAt();
}
@@ -150,7 +165,8 @@ public interface IdentityServiceFacade
Object getClaim(String claim);
}
class AuthorizationGrant {
class AuthorizationGrant
{
private final String username;
private final String password;
private final String refreshToken;

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -72,9 +72,8 @@ import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -98,6 +97,7 @@ import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
@@ -125,6 +125,10 @@ import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
/**
* Creates an instance of {@link IdentityServiceFacade}. <br>
* This factory can return a null if it is disabled.
@@ -134,6 +138,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
private static final String DEFAULT_ISSUER_ATTR = "issuer";
private boolean enabled;
private SpringBasedIdentityServiceFacadeFactory factory;
@@ -146,10 +151,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
{
factory = new SpringBasedIdentityServiceFacadeFactory(
new HttpClientProvider(identityServiceConfig)::createHttpClient,
new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder
);
new HttpClientProvider(identityServiceConfig)::createHttpClient,
new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder);
}
@Override
@@ -207,9 +211,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String token, String principalAttribute)
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
{
return getTargetFacade().getUserInfo(token, principalAttribute);
return getTargetFacade().getUserInfo(token, userInfoAttrMapping);
}
@Override
@@ -221,8 +225,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private IdentityServiceFacade getTargetFacade()
{
return ofNullable(targetFacade.get())
.orElseGet(() -> targetFacade.updateAndGet(prev ->
ofNullable(prev).orElseGet(this::createTargetFacade)));
.orElseGet(() -> targetFacade.updateAndGet(prev -> ofNullable(prev).orElseGet(this::createTargetFacade)));
}
private IdentityServiceFacade createTargetFacade()
@@ -250,9 +253,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private final BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider;
SpringBasedIdentityServiceFacadeFactory(
Supplier<HttpClient> httpClientProvider,
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
Supplier<HttpClient> httpClientProvider,
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
{
this.httpClientProvider = requireNonNull(httpClientProvider);
this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
@@ -261,25 +264,25 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private IdentityServiceFacade createIdentityServiceFacade()
{
//Here we preserve the behaviour of previously used Keycloak Adapter
// 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
final ClientHttpRequestFactory httpRequestFactory = new CustomClientHttpRequestFactory(
httpClientProvider.get());
httpClientProvider.get());
final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate,
clientRegistration.getProviderDetails());
clientRegistration.getProviderDetails());
return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory),
clientRegistration, jwtDecoder);
clientRegistration, jwtDecoder);
}
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
{
final RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter(), new MappingJackson2HttpMessageConverter()));
restTemplate.setRequestFactory(requestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
@@ -323,29 +326,29 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private void applyConnectionConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
{
final ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(config.getClientConnectionTimeout(), TimeUnit.MILLISECONDS)
.setSocketTimeout(config.getClientSocketTimeout(), TimeUnit.MILLISECONDS)
.build();
.setConnectTimeout(config.getClientConnectionTimeout(), TimeUnit.MILLISECONDS)
.setSocketTimeout(config.getClientSocketTimeout(), TimeUnit.MILLISECONDS)
.build();
connectionManagerBuilder.setMaxConnTotal(config.getConnectionPoolSize());
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
}
private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
throws Exception
throws Exception
{
SSLContextBuilder sslContextBuilder = null;
if (config.isDisableTrustManager())
{
sslContextBuilder = SSLContexts.custom()
.loadTrustMaterial(TrustAllStrategy.INSTANCE);
.loadTrustMaterial(TrustAllStrategy.INSTANCE);
}
else if (isDefined(config.getTruststore()))
{
final char[] truststorePassword = asCharArray(config.getTruststorePassword(), null);
sslContextBuilder = SSLContexts.custom()
.loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
.loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
}
if (isDefined(config.getClientKeystore()))
@@ -377,9 +380,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private char[] asCharArray(String value, char... nullValue)
{
return ofNullable(value)
.filter(not(String::isBlank))
.map(String::toCharArray)
.orElse(nullValue);
.filter(not(String::isBlank))
.map(String::toCharArray)
.orElse(nullValue);
}
}
@@ -387,8 +390,6 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
private final IdentityServiceConfig config;
private static final Set<String> SCOPES = Set.of("openid", "profile", "email");
ClientRegistrationProvider(IdentityServiceConfig config)
{
this.config = requireNonNull(config);
@@ -397,16 +398,16 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
public ClientRegistration createClientRegistration(final RestOperations rest)
{
return possibleMetadataURIs()
.stream()
.map(u -> extractMetadata(rest, u))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.map(this::validateDiscoveryDocument)
.map(this::createBuilder)
.map(this::configureClientAuthentication)
.map(Builder::build)
.orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
.stream()
.map(u -> extractMetadata(rest, u))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.map(this::validateDiscoveryDocument)
.map(this::createBuilder)
.map(this::configureClientAuthentication)
.map(Builder::build)
.orElseThrow(() -> new IllegalStateException("Failed to create ClientRegistration."));
}
private OIDCProviderMetadata validateDiscoveryDocument(OIDCProviderMetadata metadata)
@@ -423,11 +424,11 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
URI metadataIssuerURI = new URI(metadata.getIssuer().getValue());
validateOIDCEndpoint(metadataIssuerURI, "Issuer");
if (StringUtils.isNotBlank(config.getIssuerUrl()) &&
!metadataIssuerURI.equals(URI.create(config.getIssuerUrl())))
!metadataIssuerURI.equals(URI.create(config.getIssuerUrl())))
{
throw new IdentityServiceException("Failed to create ClientRegistration. "
+ "The Issuer value from the OIDC Discovery Endpoint does not align with the provided Issuer. Expected `%s` but found `%s`"
.formatted(config.getIssuerUrl(), metadata.getIssuer().getValue()));
+ "The Issuer value from the OIDC Discovery Endpoint does not align with the provided Issuer. Expected `%s` but found `%s`"
.formatted(config.getIssuerUrl(), metadata.getIssuer().getValue()));
}
}
catch (URISyntaxException e)
@@ -454,37 +455,37 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
private ClientRegistration.Builder createBuilder(OIDCProviderMetadata metadata)
{
final String authUri = Optional.of(metadata)
.map(OIDCProviderMetadata::getAuthorizationEndpointURI)
.map(URI::toASCIIString)
.orElse(null);
.map(OIDCProviderMetadata::getAuthorizationEndpointURI)
.map(URI::toASCIIString)
.orElse(null);
final String issuerUri = Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ?
config.getAuthServerUrl() :
config.getIssuerUrl());
Optional<String> metadataIssuer = getMetadataIssuer(metadata, config);
final String issuerUri = metadataIssuer
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ? config.getAuthServerUrl() : config.getIssuerUrl());
final String usernameAttribute = StringUtils.isNotBlank(config.getPrincipalAttribute()) ? config.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
return ClientRegistration
.withRegistrationId("ids")
.authorizationUri(authUri)
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(issuerUri)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.scope(getSupportedScopes(metadata.getScopes()))
.providerConfigurationMetadata(createMetadata(metadata))
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
.withRegistrationId("ids")
.authorizationUri(authUri)
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
.issuerUri(issuerUri)
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
.userNameAttributeName(usernameAttribute)
.scope(getSupportedScopes(metadata.getScopes()))
.providerConfigurationMetadata(createMetadata(metadata))
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
}
private Map<String, Object> createMetadata(OIDCProviderMetadata metadata)
{
Map<String, Object> configurationMetadata = new LinkedHashMap<>();
if(metadata.getScopes() != null)
if (metadata.getScopes() != null)
{
configurationMetadata.put(SCOPES_SUPPORTED.getValue(), metadata.getScopes());
}
if(StringUtils.isNotBlank(config.getAudience()))
if (StringUtils.isNotBlank(config.getAudience()))
{
configurationMetadata.put(AUDIENCE.getValue(), config.getAudience());
}
@@ -497,17 +498,23 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
if (config.isPublicClient())
{
return builder.clientSecret(null)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST);
}
return builder.clientSecret(config.getClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
}
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
.map(Identifier::getValue)
.collect(Collectors.toSet());
return scopes.stream()
.filter(this::hasPasswordGrantScope)
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasPasswordGrantScope(Scope.Value scope)
{
return config.getPasswordGrantScopes().contains(scope.getValue());
}
private Optional<OIDCProviderMetadata> extractMetadata(RestOperations rest, URI metadataUri)
@@ -519,7 +526,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
if (r.getStatusCode() != HttpStatus.OK || !r.hasBody())
{
LOGGER.warn("Unexpected response from " + metadataUri + ". Status code: " + r.getStatusCode()
+ ", has body: " + r.hasBody() + ".");
+ ", has body: " + r.hasBody() + ".");
return Optional.empty();
}
response = r.getBody();
@@ -545,19 +552,29 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
if (StringUtils.isBlank(config.getAuthServerUrl()) && StringUtils.isBlank(config.getIssuerUrl()))
{
throw new IdentityServiceException(
"Failed to create ClientRegistration. The values of issuer url and auth server url cannot both be empty.");
"Failed to create ClientRegistration. The values of issuer url and auth server url cannot both be empty.");
}
String baseUrl = StringUtils.isNotBlank(config.getAuthServerUrl()) ?
config.getAuthServerUrl() :
config.getIssuerUrl();
String baseUrl = StringUtils.isNotBlank(config.getAuthServerUrl()) ? config.getAuthServerUrl() : config.getIssuerUrl();
return List.of(UriComponentsBuilder.fromUriString(baseUrl)
.pathSegment(".well-known", "openid-configuration")
.build().toUri());
.pathSegment(".well-known", "openid-configuration")
.build().toUri());
}
}
private static Optional<String> getMetadataIssuer(OIDCProviderMetadata metadata, IdentityServiceConfig config)
{
return DEFAULT_ISSUER_ATTR.equals(config.getIssuerAttribute()) ? Optional.of(metadata)
.map(OIDCProviderMetadata::getIssuer)
.map(Issuer::getValue)
: Optional.of(metadata)
.map(OIDCProviderMetadata::getCustomParameters)
.map(map -> map.get(config.getIssuerAttribute()))
.filter(String.class::isInstance)
.map(String.class::cast);
}
static class JwtDecoderProvider
{
private static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.RS256;
@@ -568,12 +585,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
this.config = requireNonNull(config);
this.signatureAlgorithms = ofNullable(config.getSignatureAlgorithms())
.filter(not(Set::isEmpty))
.orElseGet(() -> {
LOGGER.warn("Unable to find any valid signature algorithms in the configuration. "
+ "Using the default signature algorithm: " + DEFAULT_SIGNATURE_ALGORITHM.getName() + ".");
return Set.of(DEFAULT_SIGNATURE_ALGORITHM);
});
.filter(not(Set::isEmpty))
.orElseGet(() -> {
LOGGER.warn("Unable to find any valid signature algorithms in the configuration. "
+ "Using the default signature algorithm: " + DEFAULT_SIGNATURE_ALGORITHM.getName() + ".");
return Set.of(DEFAULT_SIGNATURE_ALGORITHM);
});
}
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
@@ -584,7 +601,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
decoder.setJwtValidator(createJwtTokenValidator(providerDetails));
decoder.setClaimSetConverter(
new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
new ClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverters()));
return decoder;
}
@@ -601,26 +618,26 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
final RSAPublicKey publicKey = parsePublicKey(config.getRealmKey());
return NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(DEFAULT_SIGNATURE_ALGORITHM)
.build();
.signatureAlgorithm(DEFAULT_SIGNATURE_ALGORITHM)
.build();
}
final String jwkSetUri = requireValidJwkSetUri(providerDetails);
final NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder decoderBuilder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
signatureAlgorithms.forEach(decoderBuilder::jwsAlgorithm);
return decoderBuilder
.restOperations(rest)
.jwtProcessorCustomizer(this::reconfigureJWKSCache)
.build();
.restOperations(rest)
.jwtProcessorCustomizer(this::reconfigureJWKSCache)
.build();
}
private void reconfigureJWKSCache(ConfigurableJWTProcessor<SecurityContext> jwtProcessor)
{
final Optional<RemoteJWKSet<SecurityContext>> jwkSource = ofNullable(jwtProcessor)
.map(ConfigurableJWTProcessor::getJWSKeySelector)
.filter(JWSVerificationKeySelector.class::isInstance)
.map(o -> (JWSVerificationKeySelector<SecurityContext>) o)
.map(JWSVerificationKeySelector::getJWKSource)
.filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet<SecurityContext>) o);
.map(ConfigurableJWTProcessor::getJWSKeySelector)
.filter(JWSVerificationKeySelector.class::isInstance)
.map(o -> (JWSVerificationKeySelector<SecurityContext>) o)
.map(JWSVerificationKeySelector::getJWKSource)
.filter(RemoteJWKSet.class::isInstance).map(o -> (RemoteJWKSet<SecurityContext>) o);
if (jwkSource.isEmpty())
{
LOGGER.warn("Not able to reconfigure the JWK Cache. Unexpected JWKSource.");
@@ -642,22 +659,22 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
final DefaultJWKSetCache cache = new DefaultJWKSetCache(config.getPublicKeyCacheTtl(), -1,
TimeUnit.SECONDS);
TimeUnit.SECONDS);
final JWKSource<SecurityContext> cachingJWKSource = new RemoteJWKSet<>(jwkSetUrl.get(),
resourceRetriever.get(), cache);
resourceRetriever.get(), cache);
jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(
signatureAlgorithms.stream()
.map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName()))
.collect(Collectors.toSet()),
cachingJWKSource));
signatureAlgorithms.stream()
.map(signatureAlgorithm -> JWSAlgorithm.parse(signatureAlgorithm.getName()))
.collect(Collectors.toSet()),
cachingJWKSource));
jwtProcessor.setJWSTypeVerifier(new CustomJOSEObjectTypeVerifier(JOSEObjectType.JWT, AT_JWT));
}
private OAuth2TokenValidator<Jwt> createJwtTokenValidator(ProviderDetails providerDetails)
{
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator(Duration.of(0, ChronoUnit.MILLIS)));
validators.add(new JwtTimestampValidator(Duration.of(config.getJwtClockSkewMs(), ChronoUnit.MILLIS)));
validators.add(new JwtIssuerValidator(providerDetails.getIssuerUri()));
if (!config.isClientIdValidationDisabled())
{
@@ -680,7 +697,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
{
if (isPemFormatException(e))
{
//For backward compatibility with Keycloak adapter
// For backward compatibility with Keycloak adapter
return tryToParsePublicKey("-----BEGIN PUBLIC KEY-----\n" + pem + "\n-----END PUBLIC KEY-----");
}
throw e;
@@ -704,10 +721,10 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
if (!isDefined(uri))
{
OAuth2Error oauth2Error = new OAuth2Error("missing_signature_verifier",
"Failed to find a Signature Verifier for: '"
+ providerDetails.getIssuerUri()
+ "'. Check to ensure you have configured the JwkSet URI.",
null);
"Failed to find a Signature Verifier for: '"
+ providerDetails.getIssuerUri()
+ "'. Check to ensure you have configured the JwkSet URI.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return uri;
@@ -734,9 +751,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
}
final OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_TOKEN,
"The iss claim is not valid. Expected `%s` but got `%s`.".formatted(requiredIssuer, issuer),
"https://tools.ietf.org/html/rfc6750#section-3.1");
OAuth2ErrorCodes.INVALID_TOKEN,
"The iss claim is not valid. Expected `%s` but got `%s`.".formatted(requiredIssuer, issuer),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
}
@@ -758,20 +775,20 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
final Object audience = token.getClaim(JwtClaimNames.AUD);
if (audience != null)
{
if(audience instanceof List && ((List<String>) audience).contains(configuredAudience))
if (audience instanceof List && ((List<String>) audience).contains(configuredAudience))
{
return OAuth2TokenValidatorResult.success();
}
if(audience instanceof String && audience.equals(configuredAudience))
if (audience instanceof String && audience.equals(configuredAudience))
{
return OAuth2TokenValidatorResult.success();
}
}
final OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_TOKEN,
"The aud claim is not valid. Expected configured audience `%s` not found.".formatted(configuredAudience),
"https://tools.ietf.org/html/rfc6750#section-3.1");
OAuth2ErrorCodes.INVALID_TOKEN,
"The aud claim is not valid. Expected configured audience `%s` not found.".formatted(configuredAudience),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
}
}
@@ -786,13 +803,10 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException
{
/*
* This is to avoid the Brotli content encoding that is not well-supported by the combination of
* the Apache Http Client and the Spring RestTemplate
*/
/* This is to avoid the Brotli content encoding that is not well-supported by the combination of the Apache Http Client and the Spring RestTemplate */
ClientHttpRequest request = super.createRequest(uri, httpMethod);
request.getHeaders()
.add("Accept-Encoding", "gzip, deflate");
.add("Accept-Encoding", "gzip, deflate");
return request;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -30,59 +30,38 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapper;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.lang3.StringUtils;
/**
* This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo}
* from {@link IdentityServiceFacade.DecodedAccessToken} or {@link UserInfo}
* and creates a new user if it does not exist in the repository.
* This class handles Just in Time user provisioning. It extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository.
*/
public class IdentityServiceJITProvisioningHandler
{
private final IdentityServiceConfig identityServiceConfig;
private final IdentityServiceFacade identityServiceFacade;
private final PersonService personService;
private final TransactionService transactionService;
private final BiFunction<DecodedAccessToken, String, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = (token, usernameMappingClaim) -> {
Optional<String> firstName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> lastName = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
Optional<String> email = Optional.ofNullable(token)
.map(jwtToken -> jwtToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast);
return Optional.ofNullable(token.getClaim(Optional.ofNullable(usernameMappingClaim)
.filter(StringUtils::isNotBlank)
.orElse(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)))
.filter(String.class::isInstance)
.map(String.class::cast)
.map(this::normalizeUserId)
.map(username -> new OIDCUserInfo(username, firstName.orElse(""), lastName.orElse(""), email.orElse("")));
};
private final IdentityServiceConfig identityServiceConfig;
private UserInfoAttrMapping userInfoAttrMapping;
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
PersonService personService,
TransactionService transactionService,
IdentityServiceConfig identityServiceConfig)
PersonService personService,
TransactionService transactionService,
IdentityServiceConfig identityServiceConfig)
{
this.identityServiceFacade = identityServiceFacade;
this.personService = personService;
@@ -90,94 +69,95 @@ public class IdentityServiceJITProvisioningHandler
this.identityServiceConfig = identityServiceConfig;
}
/**
* Extracts {@link OIDCUserInfo} from the given bearer token and creates a new user if it does not exist in the repository. Call to the UserInfo endpoint is made only if the token does not contain a username claim or if user needs to be created and some of the {@link OIDCUserInfo} fields are empty.
*/
public Optional<OIDCUserInfo> extractUserInfoAndCreateUserIfNeeded(String bearerToken)
{
Optional<OIDCUserInfo> userInfoResponse = Optional.ofNullable(bearerToken)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> extractUserInfoResponseFromAccessToken(token)
.filter(userInfo -> StringUtils.isNotEmpty(userInfo.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token)));
if (transactionService.isReadOnly() || userInfoResponse.isEmpty())
if (userInfoAttrMapping == null)
{
return userInfoResponse;
initMappers(identityServiceConfig);
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Optional<OIDCUserInfo>>()
Optional<OIDCUserInfo> oidcUserInfo = Optional.ofNullable(bearerToken)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> extractUserInfoResponseFromAccessToken(token).filter(decodedTokenUser -> StringUtils.isNotEmpty(decodedTokenUser.username()))
.or(() -> extractUserInfoResponseFromEndpoint(token, userInfoAttrMapping)))
.map(tokenUserToOIDCUserMapper::toOIDCUser);
if (transactionService.isReadOnly() || oidcUserInfo.isEmpty())
{
return oidcUserInfo;
}
return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Optional<OIDCUserInfo>>() {
@Override
public Optional<OIDCUserInfo> doWork() throws Exception
{
return userInfoResponse.map(userInfo -> {
if (userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username()))
return oidcUserInfo.map(oidcUser -> {
if (userDoesNotExistsAndCanBeCreated(oidcUser))
{
if (!userInfo.allFieldsNotEmpty())
if (!oidcUser.allFieldsNotEmpty())
{
userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
oidcUser = extractUserInfoResponseFromEndpoint(bearerToken, userInfoAttrMapping)
.map(tokenUserToOIDCUserMapper::toOIDCUser)
.orElse(oidcUser);
}
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_USERNAME, userInfo.username());
properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
properties.put(ContentModel.PROP_EMAIL, userInfo.email());
properties.put(ContentModel.PROP_ORGID, "");
properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
personService.createPerson(properties);
createPerson(oidcUser);
}
return userInfo;
return oidcUser;
});
}
}, AuthenticationUtil.getSystemUserName());
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
private void initMappers(IdentityServiceConfig identityServiceConfig)
{
this.userInfoAttrMapping = initUserInfoAttrMapping(identityServiceConfig);
this.tokenUserToOIDCUserMapper = new TokenUserToOIDCUserMapper(personService);
this.tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
private boolean userDoesNotExistsAndCanBeCreated(OIDCUserInfo userInfo)
{
return userInfo.username() != null && personService.createMissingPeople()
&& !personService.personExists(userInfo.username());
}
private Optional<DecodedTokenUser> extractUserInfoResponseFromAccessToken(String bearerToken)
{
return Optional.ofNullable(bearerToken)
.map(identityServiceFacade::decodeToken)
.flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
identityServiceConfig.getPrincipalAttribute()));
.map(identityServiceFacade::decodeToken)
.flatMap(tokenToDecodedTokenUserMapper::toDecodedTokenUser);
}
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
private Optional<DecodedTokenUser> extractUserInfoResponseFromEndpoint(String bearerToken, UserInfoAttrMapping userInfoAttrMapping)
{
return identityServiceFacade.getUserInfo(bearerToken,
StringUtils.isNotBlank(identityServiceConfig.getPrincipalAttribute()) ?
identityServiceConfig.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty())
.map(userInfo -> new OIDCUserInfo(normalizeUserId(userInfo.username()),
Optional.ofNullable(userInfo.firstName()).orElse(""),
Optional.ofNullable(userInfo.lastName()).orElse(""),
Optional.ofNullable(userInfo.email()).orElse("")));
return identityServiceFacade.getUserInfo(bearerToken, userInfoAttrMapping)
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty());
}
/**
* Normalizes a user id, taking into account existing user accounts and case sensitivity settings.
*
* @param userId the user id
* @return the string
*/
private String normalizeUserId(final String userId)
private void createPerson(OIDCUserInfo userInfo)
{
if (userId == null)
{
return null;
}
Map<QName, Serializable> properties = new HashMap<>();
properties.put(ContentModel.PROP_USERNAME, userInfo.username());
properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
properties.put(ContentModel.PROP_LASTNAME, userInfo.lastName());
properties.put(ContentModel.PROP_EMAIL, userInfo.email());
properties.put(ContentModel.PROP_ORGID, "");
properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, null);
properties.put(ContentModel.PROP_SIZE_CURRENT, 0L);
properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>()
{
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(userId);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? userId : normalized;
personService.createPerson(properties);
}
private UserInfoAttrMapping initUserInfoAttrMapping(IdentityServiceConfig identityServiceConfig)
{
return new UserInfoAttrMapping(identityServiceFacade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
identityServiceConfig.getFirstNameAttribute(),
identityServiceConfig.getLastNameAttribute(),
identityServiceConfig.getEmailAttribute());
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,23 +25,23 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import java.util.Optional;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
/**
* A {@link RemoteUserMapper} implementation that detects and validates JWTs
* issued by the Alfresco Identity Service.
* A {@link RemoteUserMapper} implementation that detects and validates JWTs issued by the Alfresco Identity Service.
*
* @author Gavin Cornwell
*/
@@ -62,7 +62,8 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
/**
* Sets the active flag
*
* @param isEnabled true to enable the subsystem
* @param isEnabled
* true to enable the subsystem
*/
public void setActive(boolean isEnabled)
{
@@ -72,7 +73,8 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
/**
* Determines whether token validation failures are silent
*
* @param silent true to silently fail, false to throw an exception
* @param silent
* true to silently fail, false to throw an exception
*/
public void setValidationFailureSilent(boolean silent)
{
@@ -89,10 +91,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
this.jitProvisioningHandler = jitProvisioningHandler;
}
/*
* (non-Javadoc)
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(jakarta.servlet.http.HttpServletRequest)
*/
/* (non-Javadoc)
*
* @see org.alfresco.web.app.servlet.RemoteUserMapper#getRemoteUser(jakarta.servlet.http.HttpServletRequest) */
@Override
public String getRemoteUser(HttpServletRequest request)
{
@@ -107,7 +108,6 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
{
String normalizedUserId = extractUserFromHeader(request);
if (normalizedUserId != null)
{
// Normalize the user ID taking into account case sensitivity settings
@@ -131,10 +131,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
return null;
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive()
*/
/* (non-Javadoc)
*
* @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive() */
public boolean isActive()
{
return this.isEnabled;
@@ -143,7 +142,8 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
/**
* Extracts the user name from the JWT in the given request.
*
* @param request The request containing the JWT
* @param request
* The request containing the JWT
* @return The username or null if it can not be determined
*/
private String extractUserFromHeader(HttpServletRequest request)
@@ -163,8 +163,8 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
}
final Optional<String> possibleUsername = jitProvisioningHandler
.extractUserInfoAndCreateUserIfNeeded(bearerToken)
.map(OIDCUserInfo::username);
.extractUserInfoAndCreateUserIfNeeded(bearerToken)
.map(OIDCUserInfo::username);
if (possibleUsername.isEmpty())
{

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -30,20 +30,12 @@ import static java.util.Objects.requireNonNull;
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter;
@@ -58,40 +50,49 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRe
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
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.OAuth2AuthenticationException;
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.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
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 DefaultOAuth2UserService defaultOAuth2UserService;
private final ClientRegistration clientRegistration;
private final JwtDecoder jwtDecoder;
SpringBasedIdentityServiceFacade(RestOperations restOperations, ClientRegistration clientRegistration,
JwtDecoder jwtDecoder)
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, clientRegistration));
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
this.defaultOAuth2UserService = createOAuth2UserService(restOperations);
}
@Override
@@ -120,41 +121,18 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
@Override
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter, String principalAttribute)
public Optional<DecodedTokenUser> getUserInfo(String token, UserInfoAttrMapping userInfoAttrMapping)
{
return Optional.ofNullable(tokenParameter)
.filter(Predicate.not(String::isEmpty))
.flatMap(token -> Optional.ofNullable(clientRegistration)
.map(ClientRegistration::getProviderDetails)
.map(ClientRegistration.ProviderDetails::getUserInfoEndpoint)
.map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUri)
.flatMap(uri -> {
try
{
return Optional.of(
new UserInfoRequest(new URI(uri), new BearerAccessToken(token)).toHTTPRequest().send());
}
catch (IOException | URISyntaxException e)
{
LOGGER.warn("Failed to get user information. Reason: " + e.getMessage());
return Optional.empty();
}
})
.flatMap(httpResponse -> {
try
{
return Optional.of(UserInfoResponse.parse(httpResponse));
}
catch (ParseException e)
{
LOGGER.warn("Failed to parse user info response. Reason: " + e.getMessage());
return Optional.empty();
}
})
.map(UserInfoResponse::toSuccessResponse)
.map(UserInfoSuccessResponse::getUserInfo))
.map(userInfo -> new OIDCUserInfo(userInfo.getStringClaim(principalAttribute), userInfo.getGivenName(),
userInfo.getFamilyName(), userInfo.getEmailAddress()));
try
{
return Optional.ofNullable(defaultOAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, getSpringAccessToken(token))))
.flatMap(oAuth2User -> mapOAuth2UserToDecodedTokenUser(oAuth2User, userInfoAttrMapping));
}
catch (OAuth2AuthenticationException exception)
{
LOGGER.warn("User Info Request failed: " + exception.getMessage());
return Optional.empty();
}
}
@Override
@@ -191,30 +169,25 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
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 OAuth2AccessToken expiredAccessToken = getSpringAccessToken("JUST_FOR_FULFILLING_THE_SPRING_API");
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(grant.getRefreshToken(), null);
return new OAuth2RefreshTokenGrantRequest(clientRegistration, expiredAccessToken, refreshToken,
clientRegistration.getScopes());
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()
);
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);
}
@@ -233,7 +206,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createAuthorizationCodeClient(
RestOperations rest)
RestOperations rest)
{
final DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
client.setRestOperations(rest);
@@ -241,34 +214,54 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
}
private static OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> createRefreshTokenClient(
RestOperations rest)
RestOperations rest)
{
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
client.setRestOperations(rest);
return client;
}
private static DefaultOAuth2UserService createOAuth2UserService(RestOperations rest)
{
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
userService.setRestOperations(rest);
return userService;
}
private Optional<DecodedTokenUser> mapOAuth2UserToDecodedTokenUser(OAuth2User oAuth2User, UserInfoAttrMapping userInfoAttrMapping)
{
Optional<String> preferredUsername = Optional.ofNullable(oAuth2User.getAttribute(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
.filter(String.class::isInstance)
.map(String.class::cast)
.filter(StringUtils::isNotEmpty);
Optional<String> userName = Optional.ofNullable(oAuth2User.getName()).filter(username -> !username.isEmpty()).or(() -> preferredUsername);
return userName.map(name -> DecodedTokenUser.validateAndCreate(name,
oAuth2User.getAttribute(userInfoAttrMapping.firstNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.lastNameClaim()),
oAuth2User.getAttribute(userInfoAttrMapping.emailClaim())));
}
private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest,
ClientRegistration clientRegistration)
ClientRegistration clientRegistration)
{
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(rest);
Optional.of(clientRegistration)
.map(ClientRegistration::getProviderDetails)
.map(ProviderDetails::getConfigurationMetadata)
.map(metadata -> metadata.get(AUDIENCE.getValue()))
.filter(String.class::isInstance)
.map(String.class::cast)
.ifPresent(audienceValue -> {
final OAuth2PasswordGrantRequestEntityConverter requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(audienceParameterConverter(audienceValue));
client.setRequestEntityConverter(requestEntityConverter);
});
.map(ClientRegistration::getProviderDetails)
.map(ProviderDetails::getConfigurationMetadata)
.map(metadata -> metadata.get(AUDIENCE.getValue()))
.filter(String.class::isInstance)
.map(String.class::cast)
.ifPresent(audienceValue -> {
final OAuth2PasswordGrantRequestEntityConverter requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(audienceParameterConverter(audienceValue));
client.setRequestEntityConverter(requestEntityConverter);
});
return client;
}
private static Converter<OAuth2PasswordGrantRequest, MultiValueMap<String, String>> audienceParameterConverter(
String audienceValue)
String audienceValue)
{
return (grantRequest) -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
@@ -278,6 +271,16 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
};
}
private static OAuth2AccessToken getSpringAccessToken(String token)
{
// Just for fulfilling the Spring API
return new OAuth2AccessToken(
TokenType.BEARER,
token,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST,
SOME_INSIGNIFICANT_DATE_IN_THE_PAST.plusSeconds(1));
}
private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;
@@ -297,9 +300,9 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
public String getRefreshTokenValue()
{
return Optional.of(tokenResponse)
.map(OAuth2AccessTokenResponse::getRefreshToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
.map(OAuth2AccessTokenResponse::getRefreshToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -37,13 +37,19 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.id.State;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.web.util.UriComponentsBuilder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.external.AdminConsoleAuthenticator;
@@ -53,16 +59,9 @@ import org.alfresco.repo.security.authentication.identityservice.IdentityService
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.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.web.util.UriComponentsBuilder;
/**
* An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID
* or to initiate the OIDC authorization code flow.
* An {@link AdminConsoleAuthenticator} implementation to extract an externally authenticated user ID or to initiate the OIDC authorization code flow.
*/
public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAuthenticator, ActivateableBean
{
@@ -71,7 +70,6 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
private static final Set<String> SCOPES = Set.of("openid", "profile", "email", "offline_access");
private IdentityServiceConfig identityServiceConfig;
private IdentityServiceFacade identityServiceFacade;
@@ -145,7 +143,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
try
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
authorizationCode(code, request.getRequestURL().toString()));
authorizationCode(code, request.getRequestURL().toString()));
addCookies(response, accessTokenAuthorization);
bearerToken = accessTokenAuthorization.getAccessToken().getTokenValue();
}
@@ -154,8 +152,8 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
if (LOGGER.isWarnEnabled())
{
LOGGER.warn(
"Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
exception.getMessage());
"Error while trying to retrieve a response using the Authorization Code at the Token Endpoint: {}",
exception.getMessage());
}
}
return bearerToken;
@@ -188,7 +186,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
{
cookiesService.addCookie(ALFRESCO_ACCESS_TOKEN, accessTokenAuthorization.getAccessToken().getTokenValue(), response);
cookiesService.addCookie(ALFRESCO_TOKEN_EXPIRATION, String.valueOf(
accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
accessTokenAuthorization.getAccessToken().getExpiresAt().toEpochMilli()), response);
cookiesService.addCookie(ALFRESCO_REFRESH_TOKEN, accessTokenAuthorization.getRefreshTokenValue(), response);
}
@@ -198,13 +196,13 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
State state = new State();
UriComponentsBuilder authRequestBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getAuthorizationUri())
.queryParam("client_id", clientRegistration.getClientId())
.queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
.queryParam("response_type", "code")
.queryParam("scope", String.join("+", getScopes(clientRegistration)))
.queryParam("state", state.toString());
.queryParam("client_id", clientRegistration.getClientId())
.queryParam("redirect_uri", getRedirectUri(request.getRequestURL().toString()))
.queryParam("response_type", "code")
.queryParam("scope", String.join("+", getScopes(clientRegistration)))
.queryParam("state", state.toString());
if(StringUtils.isNotBlank(identityServiceConfig.getAudience()))
if (StringUtils.isNotBlank(identityServiceConfig.getAudience()))
{
authRequestBuilder.queryParam("audience", identityServiceConfig.getAudience());
}
@@ -215,20 +213,25 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private Set<String> getScopes(ClientRegistration clientRegistration)
{
return Optional.ofNullable(clientRegistration.getProviderDetails())
.map(ProviderDetails::getConfigurationMetadata)
.map(metadata -> metadata.get(SCOPES_SUPPORTED.getValue()))
.filter(Scope.class::isInstance)
.map(Scope.class::cast)
.map(this::getSupportedScopes)
.orElse(clientRegistration.getScopes());
.map(ProviderDetails::getConfigurationMetadata)
.map(metadata -> metadata.get(SCOPES_SUPPORTED.getValue()))
.filter(Scope.class::isInstance)
.map(Scope.class::cast)
.map(this::getSupportedScopes)
.orElse(clientRegistration.getScopes());
}
private Set<String> getSupportedScopes(Scope scopes)
{
return scopes.stream()
.filter(scope -> SCOPES.contains(scope.getValue()))
.map(Identifier::getValue)
.collect(Collectors.toSet());
.filter(this::hasAdminConsoleScope)
.map(Identifier::getValue)
.collect(Collectors.toSet());
}
private boolean hasAdminConsoleScope(Scope.Value scope)
{
return identityServiceConfig.getAdminConsoleScopes().contains(scope.getValue());
}
private String getRedirectUri(String requestURL)
@@ -263,7 +266,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
private AccessTokenAuthorization doRefreshAuthToken(String refreshToken)
{
AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
AuthorizationGrant.refreshToken(refreshToken));
AuthorizationGrant.refreshToken(refreshToken));
if (accessTokenAuthorization == null || accessTokenAuthorization.getAccessToken() == null)
{
throw new AuthenticationException("AccessTokenResponse is null or empty");
@@ -284,7 +287,7 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
}
public void setIdentityServiceFacade(
IdentityServiceFacade identityServiceFacade)
IdentityServiceFacade identityServiceFacade)
{
this.identityServiceFacade = identityServiceFacade;
}
@@ -295,13 +298,13 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
}
public void setCookiesService(
AdminConsoleAuthenticationCookiesService cookiesService)
AdminConsoleAuthenticationCookiesService cookiesService)
{
this.cookiesService = cookiesService;
}
public void setIdentityServiceConfig(
IdentityServiceConfig identityServiceConfig)
IdentityServiceConfig identityServiceConfig)
{
this.identityServiceConfig = identityServiceConfig;
}

View File

@@ -0,0 +1,66 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.apache.commons.lang3.StringUtils;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapper
{
private static final String DEFAULT_USERNAME_CLAIM = PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
private final UserInfoAttrMapping userInfoAttrMapping;
public AccessTokenToDecodedTokenUserMapper(UserInfoAttrMapping userInfoAttrMapping)
{
this.userInfoAttrMapping = userInfoAttrMapping;
}
/**
* Maps the given {@link IdentityServiceFacade.DecodedAccessToken} to a {@link DecodedTokenUser}.
*
* @param token
* the token to map
* @return the mapped {@link DecodedTokenUser} or {@link Optional#empty()} if the token does not contain a username claim
*/
public Optional<DecodedTokenUser> toDecodedTokenUser(IdentityServiceFacade.DecodedAccessToken token)
{
Object firstName = token.getClaim(userInfoAttrMapping.firstNameClaim());
Object lastName = token.getClaim(userInfoAttrMapping.lastNameClaim());
Object email = token.getClaim(userInfoAttrMapping.emailClaim());
return Optional.ofNullable(token.getClaim(Optional.ofNullable(userInfoAttrMapping.usernameClaim())
.filter(StringUtils::isNotBlank)
.orElse(DEFAULT_USERNAME_CLAIM)))
.filter(String.class::isInstance)
.map(String.class::cast)
.map(username -> DecodedTokenUser.validateAndCreate(username, firstName, lastName, email));
}
}

View File

@@ -0,0 +1,109 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import java.util.Objects;
import java.util.Optional;
public final class DecodedTokenUser
{
private static final String EMPTY_STRING = "";
private final String username;
private final String firstName;
private final String lastName;
private final String email;
public DecodedTokenUser(String username, String firstName, String lastName, String email)
{
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public static DecodedTokenUser validateAndCreate(String username, Object firstName, Object lastName, Object email)
{
return new DecodedTokenUser(username, getStringVal(firstName), getStringVal(lastName), getStringVal(email));
}
private static String getStringVal(Object firstName)
{
return Optional.ofNullable(firstName).filter(String.class::isInstance).map(String.class::cast).orElse(EMPTY_STRING);
}
public String username()
{
return username;
}
public String firstName()
{
return firstName;
}
public String lastName()
{
return lastName;
}
public String email()
{
return email;
}
@Override
public boolean equals(Object object)
{
if (this == object)
{
return true;
}
if (object == null || getClass() != object.getClass())
{
return false;
}
DecodedTokenUser that = (DecodedTokenUser) object;
return Objects.equals(username, that.username) && Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email);
}
@Override
public int hashCode()
{
return Objects.hash(username, firstName, lastName, email);
}
@Override
public String toString()
{
return "DecodedTokenUser{" +
"username='" + username + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
'}';
}
}

View File

@@ -23,7 +23,7 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.repo.security.authentication.identityservice;
package org.alfresco.repo.security.authentication.identityservice.user;
import java.util.stream.Stream;

View File

@@ -0,0 +1,76 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapper
{
private final PersonService personService;
public TokenUserToOIDCUserMapper(PersonService personService)
{
this.personService = personService;
}
/**
* Maps a decoded token user to an OIDC user where the user id (username) is normalized.
*
* @param decodedTokenUser
* the decoded token user
* @return the OIDC user
*/
public OIDCUserInfo toOIDCUser(DecodedTokenUser decodedTokenUser)
{
return new OIDCUserInfo(usernameToUserId(decodedTokenUser.username()), decodedTokenUser.firstName(), decodedTokenUser.lastName(), decodedTokenUser.email());
}
/**
* Normalizes a username, taking into account existing user accounts and case sensitivity settings.
*
* @param caseSensitiveUserName
* the case-sensitive username
* @return the string
*/
private String usernameToUserId(final String caseSensitiveUserName)
{
if (caseSensitiveUserName == null)
{
return null;
}
String normalized = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<String>() {
@Override
public String doWork() throws Exception
{
return personService.getUserIdentifier(caseSensitiveUserName);
}
}, AuthenticationUtil.getSystemUserName());
return normalized == null ? caseSensitiveUserName : normalized;
}
}

View File

@@ -0,0 +1,108 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import java.util.Objects;
public final class UserInfoAttrMapping
{
private final String usernameClaim;
private final String firstNameClaim;
private final String lastNameClaim;
private final String emailClaim;
/**
* The UserInfoAttrMapping class represents the mapping of claims fetched from the UserInfo endpoint to create an Alfresco user.
*
* @param usernameClaim
* the claim that represents the username
* @param firstNameClaim
* the claim that represents the first name
* @param lastNameClaim
* the claim that represents the last name
* @param emailClaim
* the claim that represents the email
*/
public UserInfoAttrMapping(String usernameClaim, String firstNameClaim, String lastNameClaim, String emailClaim)
{
this.usernameClaim = usernameClaim;
this.firstNameClaim = firstNameClaim;
this.lastNameClaim = lastNameClaim;
this.emailClaim = emailClaim;
}
public String usernameClaim()
{
return usernameClaim;
}
public String firstNameClaim()
{
return firstNameClaim;
}
public String lastNameClaim()
{
return lastNameClaim;
}
public String emailClaim()
{
return emailClaim;
}
@Override
public boolean equals(Object object)
{
if (this == object)
{
return true;
}
if (object == null || getClass() != object.getClass())
{
return false;
}
UserInfoAttrMapping that = (UserInfoAttrMapping) object;
return Objects.equals(usernameClaim, that.usernameClaim) && Objects.equals(firstNameClaim, that.firstNameClaim) && Objects.equals(lastNameClaim, that.lastNameClaim) && Objects.equals(emailClaim, that.emailClaim);
}
@Override
public int hashCode()
{
return Objects.hash(usernameClaim, firstNameClaim, lastNameClaim, emailClaim);
}
@Override
public String toString()
{
return "UserInfoAttrMapping{" +
"usernameClaim='" + usernameClaim + '\'' +
", firstNameClaim='" + firstNameClaim + '\'' +
", lastNameClaim='" + lastNameClaim + '\'' +
", emailClaim='" + emailClaim + '\'' +
'}';
}
}

View File

@@ -149,6 +149,15 @@
<property name="principalAttribute">
<value>${identity-service.principal-attribute:preferred_username}</value>
</property>
<property name="firstNameAttribute">
<value>${identity-service.first-name-attribute:given_name}</value>
</property>
<property name="lastNameAttribute">
<value>${identity-service.last-name-attribute:family_name}</value>
</property>
<property name="emailAttribute">
<value>${identity-service.email-attribute:email}</value>
</property>
<property name="clientIdValidationDisabled">
<value>${identity-service.client-id.validation.disabled:true}</value>
</property>
@@ -158,6 +167,18 @@
<property name="signatureAlgorithms">
<value>${identity-service.signature-algorithms:RS256,PS256}</value>
</property>
<property name="adminConsoleScopes">
<value>${identity-service.admin-console.scopes:openid,profile,email,offline_access}</value>
</property>
<property name="passwordGrantScopes">
<value>${identity-service.password-grant.scopes:openid,profile,email}</value>
</property>
<property name="issuerAttribute">
<value>${identity-service.issuer-attribute:issuer}</value>
</property>
<property name="jwtClockSkewMs">
<value>${identity-service.jwt-clock-skew-ms:0}</value>
</property>
</bean>
<!-- Enable control over mapping between request and user ID -->

View File

@@ -13,3 +13,10 @@ identity-service.credentials.secret=
identity-service.public-client=true
identity-service.admin-console.redirect-path=/alfresco/s/admin/admin-communitysummary
identity-service.signature-algorithms=RS256,PS256
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.admin-console.scopes=openid,profile,email,offline_access
identity-service.password-grant.scopes=openid,profile,email
identity-service.issuer-attribute=issuer
identity-service.jwt-clock-skew-ms=0

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,10 @@
*/
package org.alfresco;
import org.junit.experimental.categories.Categories;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.alfresco.repo.security.authentication.identityservice.ClientRegistrationProviderUnitTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
@@ -33,237 +37,236 @@ import org.alfresco.repo.security.authentication.identityservice.SpringBasedIden
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleAuthenticationCookiesServiceUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.AdminConsoleHttpServletRequestWrapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.admin.IdentityServiceAdminConsoleAuthenticatorUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.AccessTokenToDecodedTokenUserMapperUnitTest;
import org.alfresco.repo.security.authentication.identityservice.user.TokenUserToOIDCUserMapperUnitTest;
import org.alfresco.util.testing.category.DBTests;
import org.alfresco.util.testing.category.NonBuildTests;
import org.junit.experimental.categories.Categories;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
/**
* All Repository project UNIT test classes (no application context) should be added to this test suite.
* Tests marked as DBTests are automatically excluded and are run as part of {@link AllDBTestsTestSuite}.
* All Repository project UNIT test classes (no application context) should be added to this test suite. Tests marked as DBTests are automatically excluded and are run as part of {@link AllDBTestsTestSuite}.
*/
@RunWith(Categories.class)
@Categories.ExcludeCategory({DBTests.class, NonBuildTests.class})
@Suite.SuiteClasses({
org.alfresco.repo.site.SiteMembershipTest.class,
org.alfresco.encryption.EncryptorTest.class,
org.alfresco.encryption.KeyStoreKeyProviderTest.class,
org.alfresco.filesys.config.ServerConfigurationBeanTest.class,
org.alfresco.filesys.repo.rules.ShuffleTest.class,
org.alfresco.opencmis.AlfrescoCmisExceptionInterceptorTest.class,
org.alfresco.repo.admin.Log4JHierarchyInitTest.class,
org.alfresco.repo.attributes.PropTablesCleanupJobTest.class,
org.alfresco.repo.cache.AbstractCacheFactoryTest.class,
org.alfresco.repo.cache.DefaultCacheFactoryTest.class,
org.alfresco.repo.cache.DefaultSimpleCacheTest.class,
org.alfresco.repo.cache.InMemoryCacheStatisticsTest.class,
org.alfresco.repo.cache.TransactionStatsTest.class,
org.alfresco.repo.cache.lookup.EntityLookupCacheTest.class,
org.alfresco.repo.calendar.CalendarHelpersTest.class,
org.alfresco.repo.copy.CopyServiceImplUnitTest.class,
org.alfresco.repo.dictionary.RepoDictionaryDAOTest.class,
org.alfresco.repo.forms.processor.node.FieldProcessorTest.class,
org.alfresco.repo.forms.processor.workflow.TaskFormProcessorTest.class,
org.alfresco.repo.forms.processor.workflow.WorkflowFormProcessorTest.class,
org.alfresco.repo.invitation.site.InviteSenderTest.class,
org.alfresco.repo.invitation.site.InviteModeratedSenderTest.class,
org.alfresco.repo.jscript.ScriptSearchTest.class,
org.alfresco.repo.lock.LockUtilsTest.class,
org.alfresco.repo.lock.mem.LockStoreImplTest.class,
org.alfresco.repo.management.CheckRequiredClassesForLoggingConsoleUnitTest.class,
org.alfresco.repo.management.subsystems.CryptodocSwitchableApplicationContextFactoryTest.class,
org.alfresco.repo.module.ModuleDetailsImplTest.class,
org.alfresco.repo.module.ModuleVersionNumberTest.class,
org.alfresco.repo.module.DeprecatedModulesValidatorTest.class,
org.alfresco.repo.node.integrity.IntegrityEventTest.class,
org.alfresco.repo.policy.MTPolicyComponentTest.class,
org.alfresco.repo.policy.PolicyComponentTest.class,
org.alfresco.repo.rendition.RenditionNodeManagerTest.class,
org.alfresco.repo.rendition.RenditionServiceImplTest.class,
org.alfresco.repo.replication.ReplicationServiceImplTest.class,
org.alfresco.repo.rule.RuleServiceImplUnitTest.class,
org.alfresco.repo.service.StoreRedirectorProxyFactoryTest.class,
org.alfresco.repo.site.RoleComparatorImplTest.class,
org.alfresco.repo.template.UnsafeMethodsTest.class,
org.alfresco.repo.tenant.MultiTAdminServiceImplTest.class,
org.alfresco.repo.thumbnail.ThumbnailServiceImplParameterTest.class,
org.alfresco.repo.transfer.ContentChunkerImplTest.class,
org.alfresco.repo.transfer.HttpClientTransmitterImplTest.class,
org.alfresco.repo.transfer.manifest.TransferManifestTest.class,
org.alfresco.repo.transfer.TransferVersionCheckerImplTest.class,
org.alfresco.service.cmr.calendar.CalendarRecurrenceHelperTest.class,
org.alfresco.service.cmr.calendar.CalendarTimezoneHelperTest.class,
org.alfresco.tools.RenameUserTest.class,
org.alfresco.util.VersionNumberTest.class,
org.alfresco.util.FileNameValidatorTest.class,
org.alfresco.util.HttpClientHelperTest.class,
org.alfresco.util.JSONtoFmModelTest.class,
org.alfresco.util.ModelUtilTest.class,
org.alfresco.util.PropertyMapTest.class,
org.alfresco.util.ValueProtectingMapTest.class,
org.alfresco.util.json.ExceptionJsonSerializerTest.class,
org.alfresco.util.collections.CollectionUtilsTest.class,
org.alfresco.util.schemacomp.DbObjectXMLTransformerTest.class,
org.alfresco.util.schemacomp.DbPropertyTest.class,
org.alfresco.util.schemacomp.DefaultComparisonUtilsTest.class,
org.alfresco.util.schemacomp.DifferenceTest.class,
org.alfresco.util.schemacomp.MultiFileDumperTest.class,
org.alfresco.util.schemacomp.RedundantDbObjectTest.class,
org.alfresco.util.schemacomp.SchemaComparatorTest.class,
org.alfresco.util.schemacomp.SchemaToXMLTest.class,
org.alfresco.util.schemacomp.ValidatingVisitorTest.class,
org.alfresco.util.schemacomp.ValidationResultTest.class,
org.alfresco.util.schemacomp.XMLToSchemaTest.class,
org.alfresco.util.schemacomp.model.ColumnTest.class,
org.alfresco.util.schemacomp.model.ForeignKeyTest.class,
org.alfresco.util.schemacomp.model.IndexTest.class,
org.alfresco.util.schemacomp.model.PrimaryKeyTest.class,
org.alfresco.util.schemacomp.model.SchemaTest.class,
org.alfresco.util.schemacomp.model.SequenceTest.class,
org.alfresco.util.schemacomp.model.TableTest.class,
org.alfresco.util.schemacomp.validator.IndexColumnsValidatorTest.class,
org.alfresco.util.schemacomp.validator.NameValidatorTest.class,
org.alfresco.util.schemacomp.validator.SchemaVersionValidatorTest.class,
org.alfresco.util.schemacomp.validator.TypeNameOnlyValidatorTest.class,
org.alfresco.util.test.OmittedTestClassFinderUnitTest.class,
org.alfresco.util.test.junitrules.RetryAtMostRuleTest.class,
org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class,
org.alfresco.repo.search.impl.solr.AbstractSolrQueryHTTPClientTest.class,
org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngineTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorLimitsTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorPermissionsTest.class,
org.alfresco.repo.search.impl.solr.DbOrIndexSwitchingQueryLanguageTest.class,
org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class,
org.alfresco.repo.search.impl.solr.SolrSQLHttpClientTest.class,
org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class,
org.alfresco.repo.search.impl.solr.SolrJSONResultTest.class,
org.alfresco.repo.search.impl.solr.SolrSQLJSONResultMetadataSetTest.class,
org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class,
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
IdentityServiceFacadeFactoryBeanTest.class,
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
IdentityServiceJITProvisioningHandlerUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
ClientRegistrationProviderUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
org.alfresco.repo.security.permissions.PermissionCheckCollectionTest.class,
org.alfresco.repo.security.sync.LDAPUserRegistryTest.class,
org.alfresco.traitextender.TraitExtenderIntegrationTest.class,
org.alfresco.traitextender.AJExtensionsCompileTest.class,
org.alfresco.repo.site.SiteMembershipTest.class,
org.alfresco.encryption.EncryptorTest.class,
org.alfresco.encryption.KeyStoreKeyProviderTest.class,
org.alfresco.filesys.config.ServerConfigurationBeanTest.class,
org.alfresco.filesys.repo.rules.ShuffleTest.class,
org.alfresco.opencmis.AlfrescoCmisExceptionInterceptorTest.class,
org.alfresco.repo.admin.Log4JHierarchyInitTest.class,
org.alfresco.repo.attributes.PropTablesCleanupJobTest.class,
org.alfresco.repo.cache.AbstractCacheFactoryTest.class,
org.alfresco.repo.cache.DefaultCacheFactoryTest.class,
org.alfresco.repo.cache.DefaultSimpleCacheTest.class,
org.alfresco.repo.cache.InMemoryCacheStatisticsTest.class,
org.alfresco.repo.cache.TransactionStatsTest.class,
org.alfresco.repo.cache.lookup.EntityLookupCacheTest.class,
org.alfresco.repo.calendar.CalendarHelpersTest.class,
org.alfresco.repo.copy.CopyServiceImplUnitTest.class,
org.alfresco.repo.dictionary.RepoDictionaryDAOTest.class,
org.alfresco.repo.forms.processor.node.FieldProcessorTest.class,
org.alfresco.repo.forms.processor.workflow.TaskFormProcessorTest.class,
org.alfresco.repo.forms.processor.workflow.WorkflowFormProcessorTest.class,
org.alfresco.repo.invitation.site.InviteSenderTest.class,
org.alfresco.repo.invitation.site.InviteModeratedSenderTest.class,
org.alfresco.repo.jscript.ScriptSearchTest.class,
org.alfresco.repo.lock.LockUtilsTest.class,
org.alfresco.repo.lock.mem.LockStoreImplTest.class,
org.alfresco.repo.management.CheckRequiredClassesForLoggingConsoleUnitTest.class,
org.alfresco.repo.management.subsystems.CryptodocSwitchableApplicationContextFactoryTest.class,
org.alfresco.repo.module.ModuleDetailsImplTest.class,
org.alfresco.repo.module.ModuleVersionNumberTest.class,
org.alfresco.repo.module.DeprecatedModulesValidatorTest.class,
org.alfresco.repo.node.integrity.IntegrityEventTest.class,
org.alfresco.repo.policy.MTPolicyComponentTest.class,
org.alfresco.repo.policy.PolicyComponentTest.class,
org.alfresco.repo.rendition.RenditionNodeManagerTest.class,
org.alfresco.repo.rendition.RenditionServiceImplTest.class,
org.alfresco.repo.replication.ReplicationServiceImplTest.class,
org.alfresco.repo.rule.RuleServiceImplUnitTest.class,
org.alfresco.repo.service.StoreRedirectorProxyFactoryTest.class,
org.alfresco.repo.site.RoleComparatorImplTest.class,
org.alfresco.repo.template.UnsafeMethodsTest.class,
org.alfresco.repo.tenant.MultiTAdminServiceImplTest.class,
org.alfresco.repo.thumbnail.ThumbnailServiceImplParameterTest.class,
org.alfresco.repo.transfer.ContentChunkerImplTest.class,
org.alfresco.repo.transfer.HttpClientTransmitterImplTest.class,
org.alfresco.repo.transfer.manifest.TransferManifestTest.class,
org.alfresco.repo.transfer.TransferVersionCheckerImplTest.class,
org.alfresco.service.cmr.calendar.CalendarRecurrenceHelperTest.class,
org.alfresco.service.cmr.calendar.CalendarTimezoneHelperTest.class,
org.alfresco.tools.RenameUserTest.class,
org.alfresco.util.VersionNumberTest.class,
org.alfresco.util.FileNameValidatorTest.class,
org.alfresco.util.HttpClientHelperTest.class,
org.alfresco.util.JSONtoFmModelTest.class,
org.alfresco.util.ModelUtilTest.class,
org.alfresco.util.PropertyMapTest.class,
org.alfresco.util.ValueProtectingMapTest.class,
org.alfresco.util.json.ExceptionJsonSerializerTest.class,
org.alfresco.util.collections.CollectionUtilsTest.class,
org.alfresco.util.schemacomp.DbObjectXMLTransformerTest.class,
org.alfresco.util.schemacomp.DbPropertyTest.class,
org.alfresco.util.schemacomp.DefaultComparisonUtilsTest.class,
org.alfresco.util.schemacomp.DifferenceTest.class,
org.alfresco.util.schemacomp.MultiFileDumperTest.class,
org.alfresco.util.schemacomp.RedundantDbObjectTest.class,
org.alfresco.util.schemacomp.SchemaComparatorTest.class,
org.alfresco.util.schemacomp.SchemaToXMLTest.class,
org.alfresco.util.schemacomp.ValidatingVisitorTest.class,
org.alfresco.util.schemacomp.ValidationResultTest.class,
org.alfresco.util.schemacomp.XMLToSchemaTest.class,
org.alfresco.util.schemacomp.model.ColumnTest.class,
org.alfresco.util.schemacomp.model.ForeignKeyTest.class,
org.alfresco.util.schemacomp.model.IndexTest.class,
org.alfresco.util.schemacomp.model.PrimaryKeyTest.class,
org.alfresco.util.schemacomp.model.SchemaTest.class,
org.alfresco.util.schemacomp.model.SequenceTest.class,
org.alfresco.util.schemacomp.model.TableTest.class,
org.alfresco.util.schemacomp.validator.IndexColumnsValidatorTest.class,
org.alfresco.util.schemacomp.validator.NameValidatorTest.class,
org.alfresco.util.schemacomp.validator.SchemaVersionValidatorTest.class,
org.alfresco.util.schemacomp.validator.TypeNameOnlyValidatorTest.class,
org.alfresco.util.test.OmittedTestClassFinderUnitTest.class,
org.alfresco.util.test.junitrules.RetryAtMostRuleTest.class,
org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class,
org.alfresco.repo.search.impl.solr.AbstractSolrQueryHTTPClientTest.class,
org.alfresco.repo.search.impl.solr.SpellCheckDecisionManagerTest.class,
org.alfresco.repo.search.impl.solr.SolrStoreMappingWrapperTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.DBQueryEngineTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorLimitsTest.class,
org.alfresco.repo.search.impl.querymodel.impl.db.NodePermissionAssessorPermissionsTest.class,
org.alfresco.repo.search.impl.solr.DbOrIndexSwitchingQueryLanguageTest.class,
org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class,
org.alfresco.repo.search.impl.solr.SolrSQLHttpClientTest.class,
org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class,
org.alfresco.repo.search.impl.solr.SolrJSONResultTest.class,
org.alfresco.repo.search.impl.solr.SolrSQLJSONResultMetadataSetTest.class,
org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class,
org.alfresco.repo.search.impl.solr.facet.FacetQNameUtilsTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.repo.solr.SOLRTrackingComponentUnitTest.class,
IdentityServiceFacadeFactoryBeanTest.class,
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
SpringBasedIdentityServiceFacadeUnitTest.class,
IdentityServiceJITProvisioningHandlerUnitTest.class,
AccessTokenToDecodedTokenUserMapperUnitTest.class,
TokenUserToOIDCUserMapperUnitTest.class,
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
ClientRegistrationProviderUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
org.alfresco.repo.security.permissions.PermissionCheckCollectionTest.class,
org.alfresco.repo.security.sync.LDAPUserRegistryTest.class,
org.alfresco.traitextender.TraitExtenderIntegrationTest.class,
org.alfresco.traitextender.AJExtensionsCompileTest.class,
org.alfresco.repo.virtual.page.PageCollatorTest.class,
org.alfresco.repo.virtual.ref.GetChildByIdMethodTest.class,
org.alfresco.repo.virtual.ref.GetParentReferenceMethodTest.class,
org.alfresco.repo.virtual.ref.NewVirtualReferenceMethodTest.class,
org.alfresco.repo.virtual.ref.PlainReferenceParserTest.class,
org.alfresco.repo.virtual.ref.PlainStringifierTest.class,
org.alfresco.repo.virtual.ref.ProtocolTest.class,
org.alfresco.repo.virtual.ref.ReferenceTest.class,
org.alfresco.repo.virtual.ref.ResourceParameterTest.class,
org.alfresco.repo.virtual.ref.StringParameterTest.class,
org.alfresco.repo.virtual.ref.VirtualProtocolTest.class,
org.alfresco.repo.virtual.store.ReferenceComparatorTest.class,
org.alfresco.repo.virtual.page.PageCollatorTest.class,
org.alfresco.repo.virtual.ref.GetChildByIdMethodTest.class,
org.alfresco.repo.virtual.ref.GetParentReferenceMethodTest.class,
org.alfresco.repo.virtual.ref.NewVirtualReferenceMethodTest.class,
org.alfresco.repo.virtual.ref.PlainReferenceParserTest.class,
org.alfresco.repo.virtual.ref.PlainStringifierTest.class,
org.alfresco.repo.virtual.ref.ProtocolTest.class,
org.alfresco.repo.virtual.ref.ReferenceTest.class,
org.alfresco.repo.virtual.ref.ResourceParameterTest.class,
org.alfresco.repo.virtual.ref.StringParameterTest.class,
org.alfresco.repo.virtual.ref.VirtualProtocolTest.class,
org.alfresco.repo.virtual.store.ReferenceComparatorTest.class,
org.alfresco.repo.virtual.ref.ZeroReferenceParserTest.class,
org.alfresco.repo.virtual.ref.ZeroStringifierTest.class,
org.alfresco.repo.virtual.ref.ZeroReferenceParserTest.class,
org.alfresco.repo.virtual.ref.ZeroStringifierTest.class,
org.alfresco.repo.virtual.ref.HashStringifierTest.class,
org.alfresco.repo.virtual.ref.NodeRefRadixHasherTest.class,
org.alfresco.repo.virtual.ref.NumericPathHasherTest.class,
org.alfresco.repo.virtual.ref.StoredPathHasherTest.class,
org.alfresco.repo.virtual.ref.HashStringifierTest.class,
org.alfresco.repo.virtual.ref.NodeRefRadixHasherTest.class,
org.alfresco.repo.virtual.ref.NumericPathHasherTest.class,
org.alfresco.repo.virtual.ref.StoredPathHasherTest.class,
org.alfresco.repo.virtual.template.VirtualQueryImplTest.class,
org.alfresco.repo.virtual.store.TypeVirtualizationMethodUnitTest.class,
org.alfresco.repo.virtual.template.VirtualQueryImplTest.class,
org.alfresco.repo.virtual.store.TypeVirtualizationMethodUnitTest.class,
org.alfresco.repo.security.authentication.AuthenticationServiceImplTest.class,
org.alfresco.util.EmailHelperTest.class,
org.alfresco.repo.action.ParameterDefinitionImplTest.class,
org.alfresco.repo.action.ActionDefinitionImplTest.class,
org.alfresco.repo.action.ActionConditionDefinitionImplTest.class,
org.alfresco.repo.action.ActionImplTest.class,
org.alfresco.repo.action.ActionConditionImplTest.class,
org.alfresco.repo.action.CompositeActionImplTest.class,
org.alfresco.repo.action.CompositeActionConditionImplTest.class,
org.alfresco.repo.action.executer.TransformActionExecuterTest.class,
org.alfresco.repo.action.executer.ImporterActionExecutorUnitTest.class,
org.alfresco.repo.audit.AuditableAnnotationTest.class,
org.alfresco.repo.audit.PropertyAuditFilterTest.class,
org.alfresco.repo.audit.access.NodeChangeTest.class,
org.alfresco.repo.content.ContentServiceImplUnitTest.class,
org.alfresco.repo.content.directurl.SystemWideDirectUrlConfigUnitTest.class,
org.alfresco.repo.content.directurl.ContentStoreDirectUrlConfigUnitTest.class,
org.alfresco.repo.content.LimitedStreamCopierTest.class,
org.alfresco.repo.content.filestore.FileIOTest.class,
org.alfresco.repo.content.filestore.SpoofedTextContentReaderTest.class,
org.alfresco.repo.content.ContentDataTest.class,
org.alfresco.repo.content.replication.AggregatingContentStoreUnitTest.class,
org.alfresco.service.cmr.repository.TransformationOptionLimitsTest.class,
org.alfresco.service.cmr.repository.TransformationOptionPairTest.class,
org.alfresco.repo.content.transform.TransformerConfigTestSuite.class,
org.alfresco.repo.content.transform.TransformerDebugTest.class,
org.alfresco.service.cmr.repository.TemporalSourceOptionsTest.class,
org.alfresco.repo.content.metadata.MetadataExtracterLimitsTest.class,
org.alfresco.repo.content.caching.quota.StandardQuotaStrategyMockTest.class,
org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategyTest.class,
org.alfresco.repo.content.caching.CachingContentStoreTest.class,
org.alfresco.repo.content.caching.ContentCacheImplTest.class,
org.alfresco.repo.domain.permissions.FixedAclUpdaterUnitTest.class,
org.alfresco.repo.domain.propval.PropertyTypeConverterTest.class,
org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
org.alfresco.repo.search.DocumentNavigatorTest.class,
org.alfresco.util.NumericEncodingTest.class,
org.alfresco.repo.search.impl.parsers.CMIS_FTSTest.class,
org.alfresco.repo.search.impl.parsers.CMISTest.class,
org.alfresco.repo.search.impl.parsers.FTSTest.class,
org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest.class,
org.alfresco.repo.security.authentication.AuthorizationTest.class,
org.alfresco.repo.security.permissions.PermissionCheckedCollectionTest.class,
org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSetTest.class,
org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterUtilsTest.class,
org.alfresco.repo.security.authentication.ChainingAuthenticationServiceTest.class,
org.alfresco.repo.security.authentication.NameBasedUserNameGeneratorTest.class,
org.alfresco.repo.version.common.VersionImplTest.class,
org.alfresco.repo.version.common.VersionHistoryImplTest.class,
org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicyTest.class,
org.alfresco.repo.workflow.activiti.WorklfowObjectFactoryTest.class,
org.alfresco.repo.workflow.activiti.properties.ActivitiPriorityPropertyHandlerTest.class,
org.alfresco.repo.workflow.WorkflowSuiteContextShutdownTest.class,
org.alfresco.repo.search.LuceneUtilsTest.class,
org.alfresco.repo.security.authentication.AuthenticationServiceImplTest.class,
org.alfresco.util.EmailHelperTest.class,
org.alfresco.repo.action.ParameterDefinitionImplTest.class,
org.alfresco.repo.action.ActionDefinitionImplTest.class,
org.alfresco.repo.action.ActionConditionDefinitionImplTest.class,
org.alfresco.repo.action.ActionImplTest.class,
org.alfresco.repo.action.ActionConditionImplTest.class,
org.alfresco.repo.action.CompositeActionImplTest.class,
org.alfresco.repo.action.CompositeActionConditionImplTest.class,
org.alfresco.repo.action.executer.TransformActionExecuterTest.class,
org.alfresco.repo.action.executer.ImporterActionExecutorUnitTest.class,
org.alfresco.repo.audit.AuditableAnnotationTest.class,
org.alfresco.repo.audit.PropertyAuditFilterTest.class,
org.alfresco.repo.audit.access.NodeChangeTest.class,
org.alfresco.repo.content.ContentServiceImplUnitTest.class,
org.alfresco.repo.content.directurl.SystemWideDirectUrlConfigUnitTest.class,
org.alfresco.repo.content.directurl.ContentStoreDirectUrlConfigUnitTest.class,
org.alfresco.repo.content.LimitedStreamCopierTest.class,
org.alfresco.repo.content.filestore.FileIOTest.class,
org.alfresco.repo.content.filestore.SpoofedTextContentReaderTest.class,
org.alfresco.repo.content.ContentDataTest.class,
org.alfresco.repo.content.replication.AggregatingContentStoreUnitTest.class,
org.alfresco.service.cmr.repository.TransformationOptionLimitsTest.class,
org.alfresco.service.cmr.repository.TransformationOptionPairTest.class,
org.alfresco.repo.content.transform.TransformerConfigTestSuite.class,
org.alfresco.repo.content.transform.TransformerDebugTest.class,
org.alfresco.service.cmr.repository.TemporalSourceOptionsTest.class,
org.alfresco.repo.content.metadata.MetadataExtracterLimitsTest.class,
org.alfresco.repo.content.caching.quota.StandardQuotaStrategyMockTest.class,
org.alfresco.repo.content.caching.quota.UnlimitedQuotaStrategyTest.class,
org.alfresco.repo.content.caching.CachingContentStoreTest.class,
org.alfresco.repo.content.caching.ContentCacheImplTest.class,
org.alfresco.repo.domain.permissions.FixedAclUpdaterUnitTest.class,
org.alfresco.repo.domain.propval.PropertyTypeConverterTest.class,
org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImplTest.class,
org.alfresco.repo.search.MLAnaysisModeExpansionTest.class,
org.alfresco.repo.search.DocumentNavigatorTest.class,
org.alfresco.util.NumericEncodingTest.class,
org.alfresco.repo.search.impl.parsers.CMIS_FTSTest.class,
org.alfresco.repo.search.impl.parsers.CMISTest.class,
org.alfresco.repo.search.impl.parsers.FTSTest.class,
org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactoryTest.class,
org.alfresco.repo.security.authentication.AuthorizationTest.class,
org.alfresco.repo.security.permissions.PermissionCheckedCollectionTest.class,
org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSetTest.class,
org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterUtilsTest.class,
org.alfresco.repo.security.authentication.ChainingAuthenticationServiceTest.class,
org.alfresco.repo.security.authentication.NameBasedUserNameGeneratorTest.class,
org.alfresco.repo.version.common.VersionImplTest.class,
org.alfresco.repo.version.common.VersionHistoryImplTest.class,
org.alfresco.repo.version.common.versionlabel.SerialVersionLabelPolicyTest.class,
org.alfresco.repo.workflow.activiti.WorklfowObjectFactoryTest.class,
org.alfresco.repo.workflow.activiti.properties.ActivitiPriorityPropertyHandlerTest.class,
org.alfresco.repo.workflow.WorkflowSuiteContextShutdownTest.class,
org.alfresco.repo.search.LuceneUtilsTest.class,
org.alfresco.heartbeat.HBDataCollectorServiceImplTest.class,
org.alfresco.heartbeat.jobs.LockingJobTest.class,
org.alfresco.heartbeat.jobs.QuartzJobSchedulerTest.class,
org.alfresco.heartbeat.AuthoritiesDataCollectorTest.class,
org.alfresco.heartbeat.ConfigurationDataCollectorTest.class,
org.alfresco.heartbeat.InfoDataCollectorTest.class,
org.alfresco.heartbeat.ModelUsageDataCollectorTest.class,
org.alfresco.heartbeat.SessionsUsageDataCollectorTest.class,
org.alfresco.heartbeat.SystemUsageDataCollectorTest.class,
org.alfresco.heartbeat.HBDataCollectorServiceImplTest.class,
org.alfresco.heartbeat.jobs.LockingJobTest.class,
org.alfresco.heartbeat.jobs.QuartzJobSchedulerTest.class,
org.alfresco.heartbeat.AuthoritiesDataCollectorTest.class,
org.alfresco.heartbeat.ConfigurationDataCollectorTest.class,
org.alfresco.heartbeat.InfoDataCollectorTest.class,
org.alfresco.heartbeat.ModelUsageDataCollectorTest.class,
org.alfresco.heartbeat.SessionsUsageDataCollectorTest.class,
org.alfresco.heartbeat.SystemUsageDataCollectorTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.util.bean.HierarchicalBeanLoaderTest.class,
org.alfresco.util.resource.HierarchicalResourceLoaderTest.class,
org.alfresco.repo.events.ClientUtilTest.class,
org.alfresco.repo.rendition2.RenditionService2Test.class,
org.alfresco.repo.rendition2.TransformationOptionsConverterTest.class,
org.alfresco.util.BeanExtenderUnitTest.class,
org.alfresco.util.bean.HierarchicalBeanLoaderTest.class,
org.alfresco.util.resource.HierarchicalResourceLoaderTest.class,
org.alfresco.repo.events.ClientUtilTest.class,
org.alfresco.repo.rendition2.RenditionService2Test.class,
org.alfresco.repo.rendition2.TransformationOptionsConverterTest.class,
org.alfresco.repo.event2.RepoEvent2UnitSuite.class,
org.alfresco.repo.event2.RepoEvent2UnitSuite.class,
org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class,
org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class,
org.alfresco.repo.serviceaccount.ServiceAccountRegistryImplTest.class
org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class,
org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class,
org.alfresco.repo.serviceaccount.ServiceAccountRegistryImplTest.class
})
public class AllUnitTestsSuite
{
}
{}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,8 +38,7 @@ import java.util.Set;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.ClientRegistrationProvider;
import net.minidev.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -51,12 +50,17 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.web.client.RestTemplate;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.ClientRegistrationProvider;
public class ClientRegistrationProviderUnitTest
{
private static final String CLIENT_ID = "alfresco";
private static final String OPENID_CONFIGURATION = "{\"token_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.serviceonline.alfresco/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.serviceonline.alfresco/alfresco/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.service.alfresco/oidc/userinfo\",\"authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.serviceonline.alfresco/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"serviceonline.alfresco\",\"cloud_graph_host_name\":\"graph.oidc.net\",\"msgraph_host\":\"graph.service.alfresco\",\"rbac_url\":\"https://pas.oidc.alfresco\"}";
private static final String DISCOVERY_PATH_SEGMENTS = "/.well-known/openid-configuration";
private static final String AUTH_SERVER = "https://login.serviceonline.alfresco";
private static final String ADMIN_CONSOLE_SCOPES = "openid,email,profile,offline_access";
private static final String PSSWD_GRANT_SCOPES = "openid,email,profile";
private static final String ISSUER_ATRR = "issuer";
private IdentityServiceConfig config;
private RestTemplate restTemplate;
@@ -70,6 +74,9 @@ public class ClientRegistrationProviderUnitTest
config = new IdentityServiceConfig();
config.setAuthServerUrl(AUTH_SERVER);
config.setResource(CLIENT_ID);
config.setAdminConsoleScopes(ADMIN_CONSOLE_SCOPES);
config.setPasswordGrantScopes(PSSWD_GRANT_SCOPES);
config.setIssuerAttribute(ISSUER_ATRR);
restTemplate = mock(RestTemplate.class);
ResponseEntity responseEntity = mock(ResponseEntity.class);
@@ -90,7 +97,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
restTemplate);
assertThat(clientRegistration).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
@@ -99,7 +106,7 @@ public class ClientRegistrationProviderUnitTest
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -112,7 +119,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
restTemplate);
assertThat(clientRegistration).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
@@ -121,7 +128,7 @@ public class ClientRegistrationProviderUnitTest
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -134,7 +141,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -148,7 +155,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -161,7 +168,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -174,7 +181,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -187,7 +194,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -200,7 +207,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@@ -215,7 +222,7 @@ public class ClientRegistrationProviderUnitTest
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
}
}
@@ -227,10 +234,10 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
restTemplate);
assertThat(
clientRegistration.getScopes().containsAll(
Set.of("openid", "profile", "email"))).isTrue();
clientRegistration.getScopes().containsAll(
Set.of("openid", "profile", "email"))).isTrue();
}
}
@@ -243,7 +250,7 @@ public class ClientRegistrationProviderUnitTest
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
restTemplate);
assertThat(clientRegistration.getScopes().size()).isEqualTo(1);
assertThat(clientRegistration.getScopes().stream().findFirst().get()).isEqualTo("openid");
}
@@ -260,7 +267,45 @@ public class ClientRegistrationProviderUnitTest
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldUseDefaultIssuerAttribute()
{
config.setIssuerUrl(null);
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0");
}
}
@Test
public void shouldUseCustomIssuerAttribute()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
config.setIssuerAttribute("access_token_issuer");
when(oidcResponse.getCustomParameters()).thenReturn(createJSONObject("access_token_issuer", "https://login.serviceonline.alfresco/alfresco/v2.0/at_trust"));
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isEqualTo("https://login.serviceonline.alfresco/alfresco/v2.0/at_trust");
}
}
private static JSONObject createJSONObject(String fieldName, String fieldValue)
{
JSONObject jsonObject = new JSONObject();
jsonObject.appendField(fieldName, fieldValue);
return jsonObject;
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -32,21 +32,23 @@ import static org.mockito.Mockito.when;
import java.net.ConnectException;
import java.util.Optional;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.AccessTokenAuthorization;
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.user.OIDCUserInfo;
import org.alfresco.repo.security.sync.UserRegistrySynchronizer;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.BaseSpringTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
{
@@ -67,7 +69,6 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
@Autowired
private PersonService personService;
private IdentityServiceJITProvisioningHandler jitProvisioning;
private IdentityServiceFacade mockIdentityServiceFacade;
@@ -92,7 +93,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
authenticationContext.clearCurrentSecurityContext();
}
@Test (expected=AuthenticationException.class)
@Test(expected = AuthenticationException.class)
public void testAuthenticationFail()
{
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
@@ -122,7 +123,7 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
}
}
@Test (expected=AuthenticationException.class)
@Test(expected = AuthenticationException.class)
public void testAuthenticationFail_otherException()
{
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
@@ -145,15 +146,15 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN"))
.thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
.thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
authComponent.authenticateImpl("username", "password".toCharArray());
// Check that the authenticated user has been set
assertEquals("User has not been set as expected.","username", authenticationContext.getCurrentUserName());
assertEquals("User has not been set as expected.", "username", authenticationContext.getCurrentUserName());
}
@Test (expected= AuthenticationException.class)
@Test(expected = AuthenticationException.class)
public void testFallthroughWhenIdentityServiceFacadeIsNull()
{
authComponent.setIdentityServiceFacade(null);

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,29 +25,29 @@
*/
package org.alfresco.repo.security.authentication.identityservice;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.BaseSpringTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@SuppressWarnings("PMD.AvoidAccessibilityAlteration")
public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
@@ -61,12 +61,12 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private final boolean isAuth0Enabled = Optional.ofNullable(System.getProperty("auth0.enabled"))
.map(Boolean::valueOf)
.orElse(false);
.map(Boolean::valueOf)
.orElse(false);
private final String userPassword = Optional.ofNullable(System.getProperty("admin.password"))
.filter(password -> isAuth0Enabled)
.orElse("password");
.filter(password -> isAuth0Enabled)
.orElse("password");
@Before
public void setup()
@@ -75,16 +75,16 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
nodeService = (NodeService) applicationContext.getBean("nodeService");
transactionService = (TransactionService) applicationContext.getBean("transactionService");
DefaultChildApplicationContextManager childApplicationContextManager = (DefaultChildApplicationContextManager) applicationContext
.getBean("Authentication");
.getBean("Authentication");
ChildApplicationContextFactory childApplicationContextFactory = childApplicationContextManager.getChildApplicationContextFactory(
"identity-service1");
"identity-service1");
identityServiceFacade = (IdentityServiceFacade) childApplicationContextFactory.getApplicationContext()
.getBean("identityServiceFacade");
.getBean("identityServiceFacade");
jitProvisioningHandler = (IdentityServiceJITProvisioningHandler) childApplicationContextFactory.getApplicationContext()
.getBean("jitProvisioningHandler");
.getBean("jitProvisioningHandler");
IdentityServiceConfig identityServiceConfig = (IdentityServiceConfig) childApplicationContextFactory.getApplicationContext()
.getBean("identityServiceConfig");
.getBean("identityServiceConfig");
identityServiceConfig.setAllowAnyHostname(true);
identityServiceConfig.setClientKeystore(null);
identityServiceConfig.setDisableTrustManager(true);
@@ -95,12 +95,11 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
{
assertFalse(personService.personExists(IDS_USERNAME));
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
accessTokenAuthorization.getAccessToken().getTokenValue());
accessTokenAuthorization.getAccessToken().getTokenValue());
NodeRef person = personService.getPerson(IDS_USERNAME);
@@ -125,23 +124,26 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertFalse(personService.personExists(IDS_USERNAME));
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization = identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(principalAttribute, "given_name", "family_name", "email");
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
ClientRegistration clientRegistration = mock(ClientRegistration.class, RETURNS_DEEP_STUBS);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(principalAttribute);
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
when(idsServiceFacadeMock.getUserInfo(accessToken, userInfoAttrMapping)).thenReturn(identityServiceFacade.getUserInfo(accessToken, userInfoAttrMapping));
when(idsServiceFacadeMock.getClientRegistration()).thenReturn(clientRegistration);
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
.getDeclaredField("identityServiceFacade");
.getDeclaredField("identityServiceFacade");
declaredField.setAccessible(true);
declaredField.set(jitProvisioningHandler, idsServiceFacadeMock);
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
accessToken);
accessToken);
declaredField.set(jitProvisioningHandler, identityServiceFacade);
@@ -153,7 +155,7 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, principalAttribute);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, userInfoAttrMapping);
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());
@@ -166,16 +168,15 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
@After
public void tearDown()
{
AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork<Void>()
{
AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork<Void>() {
@Override
public Void doWork() throws Exception
{
transactionService.getRetryingTransactionHelper()
.doInTransaction((RetryingTransactionCallback<Void>) () -> {
personService.deletePerson(IDS_USERNAME);
return null;
});
.doInTransaction((RetryingTransactionCallback<Void>) () -> {
personService.deletePerson(IDS_USERNAME);
return null;
});
return null;
}
});

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,12 +38,17 @@ import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Answers;
import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.alfresco.repo.security.authentication.identityservice.user.DecodedTokenUser;
import org.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
import org.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
public class IdentityServiceJITProvisioningHandlerUnitTest
{
@@ -51,6 +56,9 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
@Mock
private IdentityServiceFacade identityServiceFacade;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ClientRegistration clientRegistration;
@Mock
private PersonService personService;
@@ -64,11 +72,22 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
private IdentityServiceConfig identityServiceConfig;
@Mock
private OIDCUserInfo userInfo;
private DecodedTokenUser decodedTokenUser;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private UserInfoAttrMapping expectedMapping;
private static final String JWT_TOKEN = "myToken";
private static final String USERNAME = "johny123";
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Doe";
private static final String EMAIL = "johny123@email.com";
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
@@ -78,149 +97,147 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(transactionService.isReadOnly()).thenReturn(false);
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
personService, transactionService, identityServiceConfig);
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(USERNAME_CLAIM);
when(identityServiceConfig.getEmailAttribute()).thenReturn(EMAIL_CLAIM);
when(identityServiceConfig.getFirstNameAttribute()).thenReturn(FIRST_NAME_CLAIM);
when(identityServiceConfig.getLastNameAttribute()).thenReturn(LAST_NAME_CLAIM);
expectedMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
}
@Test
public void shouldExtractUserInfoForExistingUser()
{
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("johny123");
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(USERNAME);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, "nickname");
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromAccessTokenAndCreateUser()
{
when(personService.personExists("johny123")).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn("John");
when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn("Doe");
when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn("johny123@email.com");
when(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(decodedAccessToken.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME)).thenReturn(FIRST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME)).thenReturn(LAST_NAME);
when(decodedAccessToken.getClaim(PersonClaims.EMAIL_CLAIM_NAME)).thenReturn(EMAIL);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade, personService, transactionService, identityServiceConfig);
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldExtractUserInfoFromUserInfoEndpointAndCreateUser()
{
when(userInfo.username()).thenReturn("johny123");
when(userInfo.firstName()).thenReturn("John");
when(userInfo.lastName()).thenReturn("Doe");
when(userInfo.email()).thenReturn("johny123@email.com");
when(personService.personExists("johny123")).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(decodedTokenUser.username()).thenReturn(USERNAME);
when(decodedTokenUser.firstName()).thenReturn(FIRST_NAME);
when(decodedTokenUser.lastName()).thenReturn(LAST_NAME);
when(decodedTokenUser.email()).thenReturn(EMAIL);
when(personService.personExists(USERNAME)).thenReturn(false);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(USERNAME);
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
assertEquals(USERNAME, result.get().username());
assertEquals(FIRST_NAME, result.get().firstName());
assertEquals(LAST_NAME, result.get().lastName());
assertEquals(EMAIL, result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(decodedTokenUser));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsername()
{
when(personService.personExists("johny123")).thenReturn(true);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
public void shouldCallUserInfoEndpointToGetUsernameWithProvidedPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, "nickname")).thenReturn(Optional.of(userInfo));
when(identityServiceConfig.getPrincipalAttribute()).thenReturn(USERNAME_CLAIM);
when(personService.personExists(USERNAME)).thenReturn(true);
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("");
when(identityServiceFacade.getUserInfo(JWT_TOKEN, expectedMapping)).thenReturn(Optional.of(DecodedTokenUser.validateAndCreate(USERNAME, null, null, null)));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
JWT_TOKEN);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals(USERNAME, result.get().username());
assertEquals("", result.get().firstName());
assertEquals("", result.get().lastName());
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, "nickname");
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, expectedMapping);
}
@Test
@@ -232,8 +249,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
verify(identityServiceFacade, never()).getUserInfo(null, expectedMapping);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -34,17 +34,18 @@ import java.time.Instant;
import java.util.Map;
import java.util.Vector;
import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletRequest;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import jakarta.servlet.http.HttpServletRequest;
import junit.framework.TestCase;
import org.mockito.Mockito;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.alfresco.repo.security.authentication.AuthenticationException;
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.alfresco.service.transaction.TransactionService;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
/**
* Tests the Identity Service based authentication subsystem.
@@ -68,7 +69,9 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
public void testWrongTokenWithSilentValidation()
{
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected ");}));
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {
throw new TokenDecodingException("Expected ");
}));
mapper.setValidationFailureSilent(true);
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
@@ -79,7 +82,9 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
public void testWrongTokenWithoutSilentValidation()
{
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {throw new TokenDecodingException("Expected");}));
final IdentityServiceRemoteUserMapper mapper = givenMapper(Map.of("WrOnG-ToKeN", () -> {
throw new TokenDecodingException("Expected");
}));
mapper.setValidationFailureSilent(false);
HttpServletRequest mockRequest = createMockTokenRequest("WrOnG-ToKeN");
@@ -92,12 +97,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
{
final TransactionService transactionService = mock(TransactionService.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class, Mockito.RETURNS_DEEP_STUBS);
final PersonService personService = mock(PersonService.class);
final IdentityServiceConfig identityServiceConfig = mock(IdentityServiceConfig.class);
when(transactionService.isReadOnly()).thenReturn(true);
when(facade.decodeToken(anyString()))
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
when(facade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
.thenReturn(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
@@ -108,14 +115,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
mapper.setActive(true);
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
return mapper;
}
/**
* Utility method for creating a mocked Servlet request with a token.
*
* @param token The token to add to the Authorization header
* @param token
* The token to add to the Authorization header
* @return The mocked request object
*/
private HttpServletRequest createMockTokenRequest(String token)

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -31,20 +31,23 @@ 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.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.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.web.client.RestOperations;
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.alfresco.repo.security.authentication.identityservice.user.UserInfoAttrMapping;
public class SpringBasedIdentityServiceFacadeUnitTest
{
private static final String USER_NAME = "user";
private static final String PASSWORD = "password";
private static final String TOKEN = "tEsT-tOkEn";
private static final UserInfoAttrMapping USER_INFO_ATTR_MAPPING = new UserInfoAttrMapping("preferred_username", "given_name", "family_name", "email");
@Test
public void shouldThrowVerificationExceptionOnFailure()
@@ -74,7 +77,6 @@ public class SpringBasedIdentityServiceFacadeUnitTest
.havingCause().withNoCause().withMessage("Expected");
}
@Test
public void shouldReturnEmptyOptionalOnFailure()
{
@@ -82,17 +84,16 @@ public class SpringBasedIdentityServiceFacadeUnitTest
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
assertThat(facade.getUserInfo(TOKEN, USER_INFO_ATTR_MAPPING).isEmpty()).isTrue();
}
private ClientRegistration testRegistration()
{
return ClientRegistration.withRegistrationId("test")
.tokenUri("http://localhost")
.clientId("test")
.userInfoUri("http://localhost/userinfo")
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
.tokenUri("http://localhost")
.clientId("test")
.userInfoUri("http://localhost/userinfo")
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.build();
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -38,18 +38,11 @@ import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import com.nimbusds.oauth2.sdk.Scope;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
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 com.nimbusds.oauth2.sdk.Scope;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -58,6 +51,14 @@ import org.mockito.Mock;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
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;
@SuppressWarnings("PMD.AvoidStringBufferField")
public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
@@ -118,7 +119,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
String.valueOf(Instant.now().plusSeconds(60).toEpochMilli()));
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);
@@ -134,7 +135,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(accessToken.getTokenValue()).thenReturn("REFRESHED_JWT_TOKEN");
when(accessToken.getExpiresAt()).thenReturn(Instant.now().plusSeconds(60));
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
@@ -155,10 +156,11 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
{
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
.formatted("http://localhost:8080", redirectPath);
.formatted("http://localhost:8080", redirectPath);
authenticator.requestAuthentication(request, response);
@@ -178,9 +180,10 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAudience()).thenReturn(audience);
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
.formatted("http://localhost:8080", redirectPath);
.formatted("http://localhost:8080", redirectPath);
authenticator.requestAuthentication(request, response);
@@ -200,7 +203,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(cookiesService.getCookie(ALFRESCO_ACCESS_TOKEN, request)).thenReturn("EXPIRED_JWT_TOKEN");
when(cookiesService.getCookie(ALFRESCO_REFRESH_TOKEN, request)).thenReturn("REFRESH_TOKEN");
when(cookiesService.getCookie(ALFRESCO_TOKEN_EXPIRATION, request)).thenReturn(
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
String.valueOf(Instant.now().minusSeconds(60).toEpochMilli()));
when(identityServiceFacade.authorize(any(AuthorizationGrant.class))).thenThrow(AuthorizationException.class);
@@ -221,8 +224,8 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
when(accessTokenAuthorization.getAccessToken()).thenReturn(accessToken);
when(accessTokenAuthorization.getRefreshTokenValue()).thenReturn("REFRESH_TOKEN");
when(identityServiceFacade.authorize(
AuthorizationGrant.authorizationCode("auth_code", adminConsoleURL.toString())))
.thenReturn(accessTokenAuthorization);
AuthorizationGrant.authorizationCode("auth_code", adminConsoleURL.toString())))
.thenReturn(accessTokenAuthorization);
when(remoteUserMapper.getRemoteUser(requestCaptor.capture())).thenReturn("admin");
String username = authenticator.getAdminConsoleUser(request, response);

View File

@@ -0,0 +1,109 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
public class AccessTokenToDecodedTokenUserMapperUnitTest
{
@Mock
private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
private AccessTokenToDecodedTokenUserMapper tokenToDecodedTokenUserMapper;
public static final String USERNAME_CLAIM = "nickname";
public static final String EMAIL_CLAIM = "email";
public static final String FIRST_NAME_CLAIM = "given_name";
public static final String LAST_NAME_CLAIM = "family_name";
@Before
public void setup()
{
initMocks(this);
UserInfoAttrMapping userInfoAttrMapping = new UserInfoAttrMapping(USERNAME_CLAIM, FIRST_NAME_CLAIM, LAST_NAME_CLAIM, EMAIL_CLAIM);
tokenToDecodedTokenUserMapper = new AccessTokenToDecodedTokenUserMapper(userInfoAttrMapping);
}
@Test
public void shouldMapToDecodedTokenUserWithAllFieldsPopulated()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("John", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("johny123@email.com", result.get().email());
}
@Test
public void shouldMapToDecodedTokenUserWithSomeFieldsEmpty()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn("johny123");
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertEquals("", result.get().firstName());
assertEquals("Doe", result.get().lastName());
assertEquals("", result.get().email());
}
@Test
public void shouldReturnEmptyOptionalForNullUsername()
{
when(decodedAccessToken.getClaim(USERNAME_CLAIM)).thenReturn(null);
when(decodedAccessToken.getClaim(FIRST_NAME_CLAIM)).thenReturn("John");
when(decodedAccessToken.getClaim(LAST_NAME_CLAIM)).thenReturn("Doe");
when(decodedAccessToken.getClaim(EMAIL_CLAIM)).thenReturn("johny123@email.com");
Optional<DecodedTokenUser> result = tokenToDecodedTokenUserMapper.toDecodedTokenUser(decodedAccessToken);
assertFalse(result.isPresent());
}
}

View File

@@ -0,0 +1,95 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2025 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.user;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.alfresco.service.cmr.security.PersonService;
public class TokenUserToOIDCUserMapperUnitTest
{
@Mock
private PersonService personService;
@InjectMocks
private TokenUserToOIDCUserMapper tokenUserToOIDCUserMapper;
@Before
public void setup()
{
initMocks(this);
}
@Test
public void shouldMapToOIDCUserWithAllFieldsPopulated()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("JOHNY123", "John", "Doe", "johny123@email.com");
when(personService.getUserIdentifier("JOHNY123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
@Test
public void shouldMapToOIDCUserWithSomeFieldsEmpty()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser("johny123", "", "Doe", "");
when(personService.getUserIdentifier("johny123")).thenReturn("johny123");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertEquals("johny123", oidcUserInfo.username());
assertEquals("", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("", oidcUserInfo.email());
}
@Test
public void shouldReturnNullForNullUsername()
{
DecodedTokenUser decodedTokenUser = new DecodedTokenUser(null, "John", "Doe", "johny123@email.com");
OIDCUserInfo oidcUserInfo = tokenUserToOIDCUserMapper.toOIDCUser(decodedTokenUser);
assertNull(oidcUserInfo.username());
assertEquals("John", oidcUserInfo.firstName());
assertEquals("Doe", oidcUserInfo.lastName());
assertEquals("johny123@email.com", oidcUserInfo.email());
}
}

View File

@@ -28,6 +28,9 @@ identity-service.register-node-at-startup=true
identity-service.register-node-period=50
identity-service.token-store=SESSION
identity-service.principal-attribute=preferred_username
identity-service.first-name-attribute=given_name
identity-service.last-name-attribute=family_name
identity-service.email-attribute=email
identity-service.turn-off-change-session-id-on-login=true
identity-service.token-minimum-time-to-live=10
identity-service.min-time-between-jwks-requests=60