mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS-9414 Enhance the Identity Provider configuration (#3263)
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -147,7 +147,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\""
|
||||
|
||||
|
@@ -1607,7 +1607,7 @@
|
||||
"filename": "repository/src/test/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacadeUnitTest.java",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 47,
|
||||
"line_number": 48,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -1868,5 +1868,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-03-17T14:00:53Z"
|
||||
"generated_at": "2025-03-21T13:01:19Z"
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ 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.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@@ -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;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -329,4 +336,78 @@ public class IdentityServiceConfig
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
*/
|
||||
@@ -66,11 +69,11 @@ public interface IdentityServiceFacade
|
||||
*
|
||||
* @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.
|
||||
* @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
|
||||
|
@@ -72,6 +72,7 @@ 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.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -96,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;
|
||||
@@ -124,6 +126,8 @@ 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>
|
||||
@@ -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;
|
||||
@@ -206,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
|
||||
@@ -277,7 +282,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
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());
|
||||
|
||||
@@ -385,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);
|
||||
@@ -456,11 +459,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
.map(URI::toASCIIString)
|
||||
.orElse(null);
|
||||
|
||||
final String issuerUri = Optional.of(metadata)
|
||||
.map(OIDCProviderMetadata::getIssuer)
|
||||
.map(Issuer::getValue)
|
||||
var metadataIssuer = getMetadataIssuer(metadata, config);
|
||||
final String issuerUri = metadataIssuer
|
||||
.orElseGet(() -> (StringUtils.isNotBlank(config.getRealm()) && StringUtils.isBlank(config.getIssuerUrl())) ? config.getAuthServerUrl() : config.getIssuerUrl());
|
||||
|
||||
final var usernameAttribute = StringUtils.isNotBlank(config.getPrincipalAttribute()) ? config.getPrincipalAttribute() : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
|
||||
|
||||
return ClientRegistration
|
||||
.withRegistrationId("ids")
|
||||
.authorizationUri(authUri)
|
||||
@@ -468,6 +472,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
||||
.issuerUri(issuerUri)
|
||||
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
|
||||
.userNameAttributeName(usernameAttribute)
|
||||
.scope(getSupportedScopes(metadata.getScopes()))
|
||||
.providerConfigurationMetadata(createMetadata(metadata))
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
|
||||
@@ -501,11 +506,17 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
|
||||
private Set<String> getSupportedScopes(Scope scopes)
|
||||
{
|
||||
return scopes.stream().filter(scope -> SCOPES.contains(scope.getValue()))
|
||||
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)
|
||||
{
|
||||
final String response;
|
||||
@@ -552,6 +563,18 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -651,7 +674,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
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())
|
||||
{
|
||||
|
@@ -30,52 +30,33 @@ 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;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -88,31 +69,77 @@ 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<>() {
|
||||
@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);
|
||||
}
|
||||
createPerson(oidcUser);
|
||||
}
|
||||
return oidcUser;
|
||||
});
|
||||
}
|
||||
|
||||
}, AuthenticationUtil.getSystemUserName());
|
||||
}
|
||||
|
||||
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(tokenToDecodedTokenUserMapper::toDecodedTokenUser);
|
||||
}
|
||||
|
||||
private Optional<DecodedTokenUser> extractUserInfoResponseFromEndpoint(String bearerToken, UserInfoAttrMapping userInfoAttrMapping)
|
||||
{
|
||||
return identityServiceFacade.getUserInfo(bearerToken, userInfoAttrMapping)
|
||||
.filter(userInfo -> userInfo.username() != null && !userInfo.username().isEmpty());
|
||||
}
|
||||
|
||||
private void createPerson(OIDCUserInfo userInfo)
|
||||
{
|
||||
Map<QName, Serializable> properties = new HashMap<>();
|
||||
properties.put(ContentModel.PROP_USERNAME, userInfo.username());
|
||||
properties.put(ContentModel.PROP_FIRSTNAME, userInfo.firstName());
|
||||
@@ -120,60 +147,17 @@ public class IdentityServiceJITProvisioningHandler
|
||||
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);
|
||||
}
|
||||
return userInfo;
|
||||
});
|
||||
}
|
||||
}, AuthenticationUtil.getSystemUserName());
|
||||
}
|
||||
|
||||
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
|
||||
private UserInfoAttrMapping initUserInfoAttrMapping(IdentityServiceConfig identityServiceConfig)
|
||||
{
|
||||
return Optional.ofNullable(bearerToken)
|
||||
.map(identityServiceFacade::decodeToken)
|
||||
.flatMap(decodedToken -> mapTokenToUserInfoResponse.apply(decodedToken,
|
||||
identityServiceConfig.getPrincipalAttribute()));
|
||||
return new UserInfoAttrMapping(identityServiceFacade.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
|
||||
identityServiceConfig.getFirstNameAttribute(),
|
||||
identityServiceConfig.getLastNameAttribute(),
|
||||
identityServiceConfig.getEmailAttribute());
|
||||
}
|
||||
|
||||
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
|
||||
{
|
||||
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("")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (userId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -38,6 +38,7 @@ 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.alfresco.repo.security.authentication.identityservice.user.OIDCUserInfo;
|
||||
|
||||
/**
|
||||
* A {@link RemoteUserMapper} implementation that detects and validates JWTs issued by the Alfresco Identity Service.
|
||||
|
@@ -30,21 +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.ErrorObject;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||
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;
|
||||
@@ -59,27 +50,35 @@ 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;
|
||||
|
||||
@@ -93,6 +92,7 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||
AuthorizationGrantType.AUTHORIZATION_CODE, createAuthorizationCodeClient(restOperations),
|
||||
AuthorizationGrantType.REFRESH_TOKEN, createRefreshTokenClient(restOperations),
|
||||
AuthorizationGrantType.PASSWORD, createPasswordClient(restOperations, clientRegistration));
|
||||
this.defaultOAuth2UserService = createOAuth2UserService(restOperations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,51 +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());
|
||||
return Optional.ofNullable(defaultOAuth2UserService.loadUser(new OAuth2UserRequest(clientRegistration, getSpringAccessToken(token))))
|
||||
.flatMap(oAuth2User -> mapOAuth2UserToDecodedTokenUser(oAuth2User, userInfoAttrMapping));
|
||||
}
|
||||
catch (IOException | URISyntaxException e)
|
||||
catch (OAuth2AuthenticationException exception)
|
||||
{
|
||||
LOGGER.warn("Failed to get user information. Reason: " + e.getMessage());
|
||||
LOGGER.warn("User Info Request failed: " + exception.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
})
|
||||
.flatMap(httpResponse -> {
|
||||
try
|
||||
{
|
||||
UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
|
||||
|
||||
if (userInfoResponse instanceof UserInfoErrorResponse userInfoErrorResponse)
|
||||
{
|
||||
String errorMessage = Optional.ofNullable(userInfoErrorResponse.getErrorObject())
|
||||
.map(ErrorObject::getDescription)
|
||||
.orElse("No error description found");
|
||||
LOGGER.warn("User Info Request failed: " + errorMessage);
|
||||
throw new UserInfoException(errorMessage);
|
||||
}
|
||||
return Optional.of(userInfoResponse);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,11 +169,7 @@ 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,
|
||||
@@ -258,6 +221,26 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
||||
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)
|
||||
{
|
||||
var preferredUsername = Optional.ofNullable(oAuth2User.getAttribute(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME))
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.filter(StringUtils::isNotEmpty);
|
||||
var 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)
|
||||
{
|
||||
@@ -288,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;
|
||||
|
@@ -70,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;
|
||||
@@ -225,11 +224,16 @@ public class IdentityServiceAdminConsoleAuthenticator implements AdminConsoleAut
|
||||
private Set<String> getSupportedScopes(Scope scopes)
|
||||
{
|
||||
return scopes.stream()
|
||||
.filter(scope -> SCOPES.contains(scope.getValue()))
|
||||
.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)
|
||||
{
|
||||
try
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* #%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;
|
||||
|
||||
public record DecodedTokenUser(String username, String firstName, String lastName, String email)
|
||||
{
|
||||
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* #%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;
|
||||
|
||||
/**
|
||||
* The UserInfoAttrMapping record 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 record UserInfoAttrMapping(String usernameClaim, String firstNameClaim, String lastNameClaim, String emailClaim)
|
||||
{}
|
@@ -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 -->
|
||||
|
@@ -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
|
||||
|
@@ -37,6 +37,8 @@ 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;
|
||||
|
||||
@@ -149,6 +151,8 @@ import org.alfresco.util.testing.category.NonBuildTests;
|
||||
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
||||
SpringBasedIdentityServiceFacadeUnitTest.class,
|
||||
IdentityServiceJITProvisioningHandlerUnitTest.class,
|
||||
AccessTokenToDecodedTokenUserMapperUnitTest.class,
|
||||
TokenUserToOIDCUserMapperUnitTest.class,
|
||||
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
|
||||
AdminConsoleHttpServletRequestWrapperUnitTest.class,
|
||||
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
|
||||
|
@@ -38,6 +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 net.minidev.json.JSONObject;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -57,6 +58,9 @@ public class ClientRegistrationProviderUnitTest
|
||||
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);
|
||||
@@ -263,4 +270,42 @@ public class ClientRegistrationProviderUnitTest
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -43,6 +43,7 @@ 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;
|
||||
|
@@ -25,10 +25,7 @@
|
||||
*/
|
||||
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;
|
||||
@@ -37,11 +34,14 @@ 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;
|
||||
@@ -126,11 +126,15 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
|
||||
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
|
||||
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()
|
||||
@@ -151,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());
|
||||
|
@@ -40,8 +40,13 @@ import java.util.Optional;
|
||||
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -38,6 +38,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||
import junit.framework.TestCase;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
|
||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||
@@ -96,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));
|
||||
|
||||
|
@@ -40,12 +40,14 @@ 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()
|
||||
@@ -82,7 +84,7 @@ 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()
|
||||
|
@@ -38,6 +38,7 @@ import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -155,6 +156,7 @@ 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="
|
||||
@@ -178,6 +180,7 @@ 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);
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user