mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS-6303 Add seamless ACS JIT user provisioning (#2336)
* ACS-6303 Implement JIT User Provisioning * ACS-6303 Fix AuthenticationsTest * ACS-6303 Add IT test * ACS-6303 Fix syntax * ACS-6303 Revert local change * ACS-6303 Update IDS version * ACS-6303 Fix JITProvisioning IT test execution * ACS-6303 Add new IT scenario * ACS-6303 Remove AppContext05TestSuite-setup.sh + optimize calling UserInfoEndpoint * ACS-6303 Fix PMD issues * ACS-6303 Fix property name * ACS-6303 Change getUserInfo return type * Apply suggestions from code review Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com> * ACS-6303 Move var declaration + use lambda+diamond operator * ACS-6303 Add a small optimisation --------- Co-authored-by: Domenico Sibilio <domenicosibilio@gmail.com>
This commit is contained in:
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -356,8 +356,8 @@ jobs:
|
|||||||
- testSuite: AppContext04TestSuite
|
- testSuite: AppContext04TestSuite
|
||||||
compose-profile: with-transform-core-aio
|
compose-profile: with-transform-core-aio
|
||||||
- testSuite: AppContext05TestSuite
|
- testSuite: AppContext05TestSuite
|
||||||
compose-profile: default
|
compose-profile: with-ids
|
||||||
mvn-options: '"-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth"'
|
mvn-options: '-Didentity-service.auth-server-url=http://${HOST_IP}:8999/auth -Dauthentication.chain=identity-service1:identity-service,alfrescoNtlm1:alfrescoNtlm'
|
||||||
- testSuite: AppContext06TestSuite
|
- testSuite: AppContext06TestSuite
|
||||||
compose-profile: with-transform-core-aio
|
compose-profile: with-transform-core-aio
|
||||||
- testSuite: AppContextExtraTestSuite
|
- testSuite: AppContextExtraTestSuite
|
||||||
@@ -381,6 +381,8 @@ jobs:
|
|||||||
run: bash ./scripts/ci/init.sh
|
run: bash ./scripts/ci/init.sh
|
||||||
- name: "Set transformers tag"
|
- name: "Set transformers tag"
|
||||||
run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV
|
run: echo "TRANSFORMERS_TAG=$(mvn help:evaluate -Dexpression=dependency.alfresco-transform-core.version -q -DforceStdout)" >> $GITHUB_ENV
|
||||||
|
- name: "Set the host IP"
|
||||||
|
run: echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV
|
||||||
- name: "Generate Keystores and Truststores for Mutual TLS configuration"
|
- name: "Generate Keystores and Truststores for Mutual TLS configuration"
|
||||||
if: ${{ matrix.mtls }}
|
if: ${{ matrix.mtls }}
|
||||||
run: |
|
run: |
|
||||||
@@ -393,11 +395,7 @@ jobs:
|
|||||||
echo "HOSTNAME_VERIFICATION_DISABLED=false" >> "$GITHUB_ENV"
|
echo "HOSTNAME_VERIFICATION_DISABLED=false" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
- name: "Set up the environment"
|
- name: "Set up the environment"
|
||||||
run: |
|
run: docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
|
||||||
if [ -e ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh ]; then
|
|
||||||
bash ./scripts/ci/tests/${{ matrix.testSuite }}-setup.sh
|
|
||||||
fi
|
|
||||||
docker-compose -f ./scripts/ci/docker-compose/docker-compose.yaml --profile ${{ matrix.compose-profile }} up -d
|
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
run: mvn -B test -pl repository -am -Dtest=${{ matrix.testSuite }} -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco ${{ matrix.mvn-options }}
|
run: mvn -B test -pl repository -am -Dtest=${{ matrix.testSuite }} -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco ${{ matrix.mvn-options }}
|
||||||
- name: "Clean Maven cache"
|
- name: "Clean Maven cache"
|
||||||
|
@@ -523,13 +523,13 @@ public class AuthenticationsTest extends AbstractSingleNetworkSiteTest
|
|||||||
private RemoteUserMapper createRemoteUserMapperToUseForTheTest(boolean useIdentityService)
|
private RemoteUserMapper createRemoteUserMapperToUseForTheTest(boolean useIdentityService)
|
||||||
{
|
{
|
||||||
PersonService personServiceLocal = (PersonService) applicationContext.getBean("PersonService");
|
PersonService personServiceLocal = (PersonService) applicationContext.getBean("PersonService");
|
||||||
|
|
||||||
RemoteUserMapper remoteUserMapper;
|
RemoteUserMapper remoteUserMapper;
|
||||||
if (useIdentityService)
|
if (useIdentityService)
|
||||||
{
|
{
|
||||||
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
|
InterceptingIdentityRemoteUserMapper interceptingRemoteUserMapper = new InterceptingIdentityRemoteUserMapper();
|
||||||
interceptingRemoteUserMapper.setActive(true);
|
interceptingRemoteUserMapper.setActive(true);
|
||||||
interceptingRemoteUserMapper.setPersonService(personServiceLocal);
|
interceptingRemoteUserMapper.setJitProvisioningHandler(null);
|
||||||
interceptingRemoteUserMapper.setIdentityServiceFacade(null);
|
|
||||||
interceptingRemoteUserMapper.setUserIdToReturn(user2);
|
interceptingRemoteUserMapper.setUserIdToReturn(user2);
|
||||||
remoteUserMapper = interceptingRemoteUserMapper;
|
remoteUserMapper = interceptingRemoteUserMapper;
|
||||||
}
|
}
|
||||||
|
@@ -50,6 +50,9 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
|||||||
private IdentityServiceFacade identityServiceFacade;
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
/** enabled flag for the identity service subsystem**/
|
/** enabled flag for the identity service subsystem**/
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
|
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
||||||
|
|
||||||
private boolean allowGuestLogin;
|
private boolean allowGuestLogin;
|
||||||
|
|
||||||
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
|
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
|
||||||
@@ -62,6 +65,12 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
|||||||
this.allowGuestLogin = allowGuestLogin;
|
this.allowGuestLogin = allowGuestLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
|
||||||
|
{
|
||||||
|
this.jitProvisioningHandler = jitProvisioningHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
|
public void authenticateImpl(String userName, char[] password) throws AuthenticationException
|
||||||
{
|
{
|
||||||
if (identityServiceFacade == null)
|
if (identityServiceFacade == null)
|
||||||
@@ -77,10 +86,13 @@ public class IdentityServiceAuthenticationComponent extends AbstractAuthenticati
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Attempt to verify user credentials
|
// Attempt to verify user credentials
|
||||||
identityServiceFacade.authorize(AuthorizationGrant.password(userName, new String(password)));
|
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."));
|
||||||
// Verification was successful so treat as authenticated user
|
// Verification was successful so treat as authenticated user
|
||||||
setCurrentUser(userName);
|
setCurrentUser(normalizedUsername);
|
||||||
}
|
}
|
||||||
catch (IdentityServiceFacadeException e)
|
catch (IdentityServiceFacadeException e)
|
||||||
{
|
{
|
||||||
|
@@ -30,6 +30,7 @@ import static java.util.Objects.requireNonNull;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to interact with the Identity Service
|
* Allows to interact with the Identity Service
|
||||||
@@ -52,6 +53,14 @@ public interface IdentityServiceFacade
|
|||||||
*/
|
*/
|
||||||
DecodedAccessToken decodeToken(String token) throws TokenDecodingException;
|
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.
|
||||||
|
* @return {@link OIDCUserInfo} containing user claims.
|
||||||
|
*/
|
||||||
|
Optional<OIDCUserInfo> getUserInfo(String token);
|
||||||
|
|
||||||
class IdentityServiceFacadeException extends RuntimeException
|
class IdentityServiceFacadeException extends RuntimeException
|
||||||
{
|
{
|
||||||
public IdentityServiceFacadeException(String message)
|
public IdentityServiceFacadeException(String message)
|
||||||
@@ -78,6 +87,20 @@ public interface IdentityServiceFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UserInfoException extends IdentityServiceFacadeException
|
||||||
|
{
|
||||||
|
|
||||||
|
UserInfoException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInfoException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TokenDecodingException extends IdentityServiceFacadeException
|
class TokenDecodingException extends IdentityServiceFacadeException
|
||||||
{
|
{
|
||||||
TokenDecodingException(String message)
|
TokenDecodingException(String message)
|
||||||
|
@@ -188,6 +188,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
return getTargetFacade().decodeToken(token);
|
return getTargetFacade().decodeToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<OIDCUserInfo> getUserInfo(String token)
|
||||||
|
{
|
||||||
|
return getTargetFacade().getUserInfo(token);
|
||||||
|
}
|
||||||
|
|
||||||
private IdentityServiceFacade getTargetFacade()
|
private IdentityServiceFacade getTargetFacade()
|
||||||
{
|
{
|
||||||
return ofNullable(targetFacade.get())
|
return ofNullable(targetFacade.get())
|
||||||
@@ -224,9 +230,9 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
|
Function<RestOperations, ClientRegistration> clientRegistrationProvider,
|
||||||
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
|
BiFunction<RestOperations, ProviderDetails, JwtDecoder> jwtDecoderProvider)
|
||||||
{
|
{
|
||||||
this.httpClientProvider = Objects.requireNonNull(httpClientProvider);
|
this.httpClientProvider = requireNonNull(httpClientProvider);
|
||||||
this.clientRegistrationProvider = Objects.requireNonNull(clientRegistrationProvider);
|
this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
|
||||||
this.jwtDecoderProvider = Objects.requireNonNull(jwtDecoderProvider);
|
this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IdentityServiceFacade createIdentityServiceFacade()
|
private IdentityServiceFacade createIdentityServiceFacade()
|
||||||
@@ -259,7 +265,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
private HttpClientProvider(IdentityServiceConfig config)
|
private HttpClientProvider(IdentityServiceConfig config)
|
||||||
{
|
{
|
||||||
this.config = Objects.requireNonNull(config);
|
this.config = requireNonNull(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClient createHttpClient()
|
private HttpClient createHttpClient()
|
||||||
@@ -354,7 +360,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
private ClientRegistrationProvider(IdentityServiceConfig config)
|
private ClientRegistrationProvider(IdentityServiceConfig config)
|
||||||
{
|
{
|
||||||
this.config = Objects.requireNonNull(config);
|
this.config = requireNonNull(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientRegistration createClientRegistration(final RestOperations rest)
|
public ClientRegistration createClientRegistration(final RestOperations rest)
|
||||||
@@ -389,6 +395,8 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
|
.tokenUri(metadata.getTokenEndpointURI().toASCIIString())
|
||||||
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
.jwkSetUri(metadata.getJWKSetURI().toASCIIString())
|
||||||
.issuerUri(issuerUri)
|
.issuerUri(issuerUri)
|
||||||
|
.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString())
|
||||||
|
.scope("openid", "profile", "email")
|
||||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +456,7 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
|||||||
|
|
||||||
JwtDecoderProvider(IdentityServiceConfig config)
|
JwtDecoderProvider(IdentityServiceConfig config)
|
||||||
{
|
{
|
||||||
this.config = Objects.requireNonNull(config);
|
this.config = requireNonNull(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
|
public JwtDecoder createJwtDecoder(RestOperations rest, ProviderDetails providerDetails)
|
||||||
|
@@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
|
* %%
|
||||||
|
* This file is part of the Alfresco software.
|
||||||
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
|
* the paid license agreement will prevail. Otherwise, the software is
|
||||||
|
* provided under the following open source license terms:
|
||||||
|
*
|
||||||
|
* Alfresco is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Alfresco is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||||
|
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
|
||||||
|
|
||||||
|
import org.alfresco.model.ContentModel;
|
||||||
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public class IdentityServiceJITProvisioningHandler
|
||||||
|
{
|
||||||
|
private final IdentityServiceFacade identityServiceFacade;
|
||||||
|
private final PersonService personService;
|
||||||
|
private final TransactionService transactionService;
|
||||||
|
|
||||||
|
private final Function<IdentityServiceFacade.DecodedAccessToken, Optional<? extends OIDCUserInfo>> mapTokenToUserInfoResponse = token -> {
|
||||||
|
Optional<String> firstName = Optional.ofNullable(token.getClaim(PersonClaims.GIVEN_NAME_CLAIM_NAME))
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast);
|
||||||
|
Optional<String> lastName = Optional.ofNullable(token.getClaim(PersonClaims.FAMILY_NAME_CLAIM_NAME))
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast);
|
||||||
|
Optional<String> email = Optional.ofNullable(token.getClaim(PersonClaims.EMAIL_CLAIM_NAME))
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast);
|
||||||
|
|
||||||
|
return Optional.ofNullable(token.getClaim(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("")));
|
||||||
|
};
|
||||||
|
|
||||||
|
public IdentityServiceJITProvisioningHandler(IdentityServiceFacade identityServiceFacade,
|
||||||
|
PersonService personService,
|
||||||
|
TransactionService transactionService)
|
||||||
|
{
|
||||||
|
this.identityServiceFacade = identityServiceFacade;
|
||||||
|
this.personService = personService;
|
||||||
|
this.transactionService = transactionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
return userInfoResponse;
|
||||||
|
}
|
||||||
|
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()))
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!userInfo.allFieldsNotEmpty())
|
||||||
|
{
|
||||||
|
userInfo = extractUserInfoResponseFromEndpoint(bearerToken).orElse(userInfo);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return userInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, AuthenticationUtil.getSystemUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<OIDCUserInfo> extractUserInfoResponseFromAccessToken(String bearerToken)
|
||||||
|
{
|
||||||
|
return Optional.ofNullable(bearerToken)
|
||||||
|
.map(identityServiceFacade::decodeToken)
|
||||||
|
.flatMap(mapTokenToUserInfoResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<OIDCUserInfo> extractUserInfoResponseFromEndpoint(String bearerToken)
|
||||||
|
{
|
||||||
|
return identityServiceFacade.getUserInfo(bearerToken)
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -32,10 +32,8 @@ import java.util.Optional;
|
|||||||
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
import org.alfresco.repo.management.subsystems.ActivateableBean;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
import org.alfresco.repo.security.authentication.AuthenticationUtil;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
|
|
||||||
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
@@ -50,19 +48,16 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenResolv
|
|||||||
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
|
public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, ActivateableBean
|
||||||
{
|
{
|
||||||
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
|
private static final Log LOGGER = LogFactory.getLog(IdentityServiceRemoteUserMapper.class);
|
||||||
static final String USERNAME_CLAIM = "preferred_username";
|
|
||||||
|
|
||||||
/** Is the mapper enabled */
|
/** Is the mapper enabled */
|
||||||
private boolean isEnabled;
|
private boolean isEnabled;
|
||||||
|
|
||||||
/** Are token validation failures handled silently? */
|
/** Are token validation failures handled silently? */
|
||||||
private boolean isValidationFailureSilent;
|
private boolean isValidationFailureSilent;
|
||||||
|
|
||||||
/** The person service. */
|
|
||||||
private PersonService personService;
|
|
||||||
|
|
||||||
private BearerTokenResolver bearerTokenResolver;
|
private BearerTokenResolver bearerTokenResolver;
|
||||||
private IdentityServiceFacade identityServiceFacade;
|
|
||||||
|
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the active flag
|
* Sets the active flag
|
||||||
@@ -83,26 +78,15 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
{
|
{
|
||||||
this.isValidationFailureSilent = silent;
|
this.isValidationFailureSilent = silent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the person service.
|
|
||||||
*
|
|
||||||
* @param personService
|
|
||||||
* the person service
|
|
||||||
*/
|
|
||||||
public void setPersonService(PersonService personService)
|
|
||||||
{
|
|
||||||
this.personService = personService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
|
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver)
|
||||||
{
|
{
|
||||||
this.bearerTokenResolver = bearerTokenResolver;
|
this.bearerTokenResolver = bearerTokenResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIdentityServiceFacade(IdentityServiceFacade identityServiceFacade)
|
public void setJitProvisioningHandler(IdentityServiceJITProvisioningHandler jitProvisioningHandler)
|
||||||
{
|
{
|
||||||
this.identityServiceFacade = identityServiceFacade;
|
this.jitProvisioningHandler = jitProvisioningHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -121,14 +105,13 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
String headerUserId = extractUserFromHeader(request);
|
String normalizedUserId = extractUserFromHeader(request);
|
||||||
|
|
||||||
if (headerUserId != null)
|
|
||||||
|
if (normalizedUserId != null)
|
||||||
{
|
{
|
||||||
// Normalize the user ID taking into account case sensitivity settings
|
// Normalize the user ID taking into account case sensitivity settings
|
||||||
String normalizedUserId = normalizeUserId(headerUserId);
|
|
||||||
LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
|
LOGGER.trace("Returning userId: " + AuthenticationUtil.maskUsername(normalizedUserId));
|
||||||
|
|
||||||
return normalizedUserId;
|
return normalizedUserId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,11 +162,9 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Optional<String> possibleUsername = Optional.ofNullable(bearerToken)
|
final Optional<String> possibleUsername = jitProvisioningHandler
|
||||||
.map(identityServiceFacade::decodeToken)
|
.extractUserInfoAndCreateUserIfNeeded(bearerToken)
|
||||||
.map(t -> t.getClaim(USERNAME_CLAIM))
|
.map(OIDCUserInfo::username);
|
||||||
.filter(String.class::isInstance)
|
|
||||||
.map(String.class::cast);
|
|
||||||
|
|
||||||
if (possibleUsername.isEmpty())
|
if (possibleUsername.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -191,39 +172,10 @@ public class IdentityServiceRemoteUserMapper implements RemoteUserMapper, Activa
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = possibleUsername.get();
|
String normalizedUsername = possibleUsername.get();
|
||||||
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(username));
|
LOGGER.trace("Extracted username: " + AuthenticationUtil.maskUsername(normalizedUsername));
|
||||||
|
|
||||||
return username;
|
return normalizedUsername;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 RunAsWork<String>()
|
|
||||||
{
|
|
||||||
public String doWork() throws Exception
|
|
||||||
{
|
|
||||||
return personService.getUserIdentifier(userId);
|
|
||||||
}
|
|
||||||
}, AuthenticationUtil.getSystemUserName());
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOGGER.debug("Normalized user name for '" + AuthenticationUtil.maskUsername(userId) + "': " + AuthenticationUtil.maskUsername(normalized));
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized == null ? userId : normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
|
* %%
|
||||||
|
* This file is part of the Alfresco software.
|
||||||
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
|
* the paid license agreement will prevail. Otherwise, the software is
|
||||||
|
* provided under the following open source license terms:
|
||||||
|
*
|
||||||
|
* Alfresco is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Alfresco is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains a set of required claims about the authentication of an End-User.
|
||||||
|
*/
|
||||||
|
public class OIDCUserInfo
|
||||||
|
{
|
||||||
|
private final String username;
|
||||||
|
private final String firstName;
|
||||||
|
private final String lastName;
|
||||||
|
private final String email;
|
||||||
|
|
||||||
|
public OIDCUserInfo(String username, String firstName, String lastName, String email)
|
||||||
|
{
|
||||||
|
this.username = username;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.lastName = lastName;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String username()
|
||||||
|
{
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String firstName()
|
||||||
|
{
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String lastName()
|
||||||
|
{
|
||||||
|
return lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String email()
|
||||||
|
{
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean allFieldsNotEmpty()
|
||||||
|
{
|
||||||
|
return Stream.of(username, firstName, lastName, email).allMatch(field -> field != null && !field.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -23,13 +23,24 @@
|
|||||||
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
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 org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
@@ -100,6 +111,42 @@ class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
|
|||||||
return new SpringAccessTokenAuthorization(response);
|
return new SpringAccessTokenAuthorization(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<OIDCUserInfo> getUserInfo(String tokenParameter)
|
||||||
|
{
|
||||||
|
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.getPreferredUsername(), userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DecodedAccessToken decodeToken(String token)
|
public DecodedAccessToken decodeToken(String token)
|
||||||
{
|
{
|
||||||
|
@@ -24,6 +24,9 @@
|
|||||||
<property name="identityServiceFacade">
|
<property name="identityServiceFacade">
|
||||||
<ref bean="identityServiceFacade"/>
|
<ref bean="identityServiceFacade"/>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="jitProvisioningHandler">
|
||||||
|
<ref bean="jitProvisioningHandler" />
|
||||||
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
|
<bean name="identityServiceFacade" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean">
|
||||||
@@ -147,17 +150,20 @@
|
|||||||
<property name="validationFailureSilent">
|
<property name="validationFailureSilent">
|
||||||
<value>${identity-service.authentication.validation.failure.silent}</value>
|
<value>${identity-service.authentication.validation.failure.silent}</value>
|
||||||
</property>
|
</property>
|
||||||
<property name="personService">
|
|
||||||
<ref bean="PersonService" />
|
|
||||||
</property>
|
|
||||||
<property name="bearerTokenResolver">
|
<property name="bearerTokenResolver">
|
||||||
<bean class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver" />
|
<bean class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver" />
|
||||||
</property>
|
</property>
|
||||||
<property name="identityServiceFacade">
|
<property name="jitProvisioningHandler">
|
||||||
<ref bean="identityServiceFacade" />
|
<ref bean="jitProvisioningHandler" />
|
||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="jitProvisioningHandler" class="org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandler">
|
||||||
|
<constructor-arg ref="PersonService"/>
|
||||||
|
<constructor-arg ref="identityServiceFacade"/>
|
||||||
|
<constructor-arg ref="transactionService"/>
|
||||||
|
</bean>
|
||||||
|
|
||||||
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
<bean id="authenticationDao" class="org.alfresco.repo.security.authentication.RepositoryAuthenticationDao">
|
||||||
<property name="nodeService" ref="nodeService" />
|
<property name="nodeService" ref="nodeService" />
|
||||||
<property name="authorityService" ref="authorityService" />
|
<property name="authorityService" ref="authorityService" />
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
package org.alfresco;
|
package org.alfresco;
|
||||||
|
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
|
||||||
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
import org.alfresco.repo.security.authentication.identityservice.SpringBasedIdentityServiceFacadeUnitTest;
|
||||||
import org.alfresco.util.testing.category.DBTests;
|
import org.alfresco.util.testing.category.DBTests;
|
||||||
@@ -143,6 +144,7 @@ import org.junit.runners.Suite;
|
|||||||
IdentityServiceFacadeFactoryBeanTest.class,
|
IdentityServiceFacadeFactoryBeanTest.class,
|
||||||
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
LazyInstantiatingIdentityServiceFacadeUnitTest.class,
|
||||||
SpringBasedIdentityServiceFacadeUnitTest.class,
|
SpringBasedIdentityServiceFacadeUnitTest.class,
|
||||||
|
IdentityServiceJITProvisioningHandlerUnitTest.class,
|
||||||
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
|
||||||
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
|
||||||
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* #%L
|
* #%L
|
||||||
* Alfresco Repository
|
* Alfresco Repository
|
||||||
* %%
|
* %%
|
||||||
* Copyright (C) 2005 - 2017 Alfresco Software Limited
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
* %%
|
* %%
|
||||||
* This file is part of the Alfresco software.
|
* This file is part of the Alfresco software.
|
||||||
* If the software was purchased under a paid Alfresco license, the terms of
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
@@ -60,6 +60,7 @@ import org.junit.runners.Suite;
|
|||||||
org.alfresco.repo.security.authentication.external.DefaultRemoteUserMapperTest.class,
|
org.alfresco.repo.security.authentication.external.DefaultRemoteUserMapperTest.class,
|
||||||
org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponentTest.class,
|
org.alfresco.repo.security.authentication.identityservice.IdentityServiceAuthenticationComponentTest.class,
|
||||||
org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapperTest.class,
|
org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapperTest.class,
|
||||||
|
org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerTest.class,
|
||||||
org.alfresco.repo.security.authentication.subsystems.SubsystemChainingFtpAuthenticatorTest.class,
|
org.alfresco.repo.security.authentication.subsystems.SubsystemChainingFtpAuthenticatorTest.class,
|
||||||
org.alfresco.repo.security.authentication.external.LocalAuthenticationServiceTest.class,
|
org.alfresco.repo.security.authentication.external.LocalAuthenticationServiceTest.class,
|
||||||
org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class,
|
org.alfresco.repo.domain.contentdata.ContentDataDAOTest.class,
|
||||||
|
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.alfresco.error.ExceptionStackUtil;
|
import org.alfresco.error.ExceptionStackUtil;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationContext;
|
import org.alfresco.repo.security.authentication.AuthenticationContext;
|
||||||
@@ -66,6 +67,8 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PersonService personService;
|
private PersonService personService;
|
||||||
|
|
||||||
|
|
||||||
|
private IdentityServiceJITProvisioningHandler jitProvisioning;
|
||||||
private IdentityServiceFacade mockIdentityServiceFacade;
|
private IdentityServiceFacade mockIdentityServiceFacade;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -77,7 +80,9 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
authComponent.setNodeService(nodeService);
|
authComponent.setNodeService(nodeService);
|
||||||
authComponent.setPersonService(personService);
|
authComponent.setPersonService(personService);
|
||||||
|
|
||||||
|
jitProvisioning = mock(IdentityServiceJITProvisioningHandler.class);
|
||||||
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
|
mockIdentityServiceFacade = mock(IdentityServiceFacade.class);
|
||||||
|
authComponent.setJitProvisioningHandler(jitProvisioning);
|
||||||
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
|
authComponent.setIdentityServiceFacade(mockIdentityServiceFacade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +139,13 @@ public class IdentityServiceAuthenticationComponentTest extends BaseSpringTest
|
|||||||
{
|
{
|
||||||
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
final AuthorizationGrant grant = AuthorizationGrant.password("username", "password");
|
||||||
AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
|
AccessTokenAuthorization authorization = mock(AccessTokenAuthorization.class);
|
||||||
|
IdentityServiceFacade.AccessToken accessToken = mock(IdentityServiceFacade.AccessToken.class);
|
||||||
|
|
||||||
|
when(authorization.getAccessToken()).thenReturn(accessToken);
|
||||||
|
when(accessToken.getTokenValue()).thenReturn("JWT_TOKEN");
|
||||||
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
|
when(mockIdentityServiceFacade.authorize(grant)).thenReturn(authorization);
|
||||||
|
when(jitProvisioning.extractUserInfoAndCreateUserIfNeeded("JWT_TOKEN"))
|
||||||
|
.thenReturn(Optional.of(new OIDCUserInfo("username", "", "", "")));
|
||||||
|
|
||||||
authComponent.authenticateImpl("username", "password".toCharArray());
|
authComponent.authenticateImpl("username", "password".toCharArray());
|
||||||
|
|
||||||
|
@@ -25,7 +25,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -33,6 +32,7 @@ import static org.mockito.Mockito.when;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -64,7 +64,7 @@ public class IdentityServiceFacadeFactoryBeanTest
|
|||||||
final Map<String, Object> claims = decodedToken.getClaims();
|
final Map<String, Object> claims = decodedToken.getClaims();
|
||||||
assertThat(claims).isNotNull()
|
assertThat(claims).isNotNull()
|
||||||
.isNotEmpty()
|
.isNotEmpty()
|
||||||
.containsEntry(USERNAME_CLAIM, "piotrek");
|
.containsEntry(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME, "piotrek");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
|
* %%
|
||||||
|
* This file is part of the Alfresco software.
|
||||||
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
|
* the paid license agreement will prevail. Otherwise, the software is
|
||||||
|
* provided under the following open source license terms:
|
||||||
|
*
|
||||||
|
* Alfresco is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Alfresco is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
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.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
|
||||||
|
{
|
||||||
|
private static final String IDS_USERNAME = "johndoe123";
|
||||||
|
|
||||||
|
private PersonService personService;
|
||||||
|
private NodeService nodeService;
|
||||||
|
private TransactionService transactionService;
|
||||||
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
|
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup()
|
||||||
|
{
|
||||||
|
personService = (PersonService) applicationContext.getBean("personService");
|
||||||
|
nodeService = (NodeService) applicationContext.getBean("nodeService");
|
||||||
|
transactionService = (TransactionService) applicationContext.getBean("transactionService");
|
||||||
|
DefaultChildApplicationContextManager childApplicationContextManager = (DefaultChildApplicationContextManager) applicationContext
|
||||||
|
.getBean("Authentication");
|
||||||
|
ChildApplicationContextFactory childApplicationContextFactory = childApplicationContextManager.getChildApplicationContextFactory(
|
||||||
|
"identity-service1");
|
||||||
|
|
||||||
|
identityServiceFacade = (IdentityServiceFacade) childApplicationContextFactory.getApplicationContext()
|
||||||
|
.getBean("identityServiceFacade");
|
||||||
|
jitProvisioningHandler = (IdentityServiceJITProvisioningHandler) childApplicationContextFactory.getApplicationContext()
|
||||||
|
.getBean("jitProvisioningHandler");
|
||||||
|
IdentityServiceConfig identityServiceConfig = (IdentityServiceConfig) childApplicationContextFactory.getApplicationContext()
|
||||||
|
.getBean("identityServiceConfig");
|
||||||
|
identityServiceConfig.setAllowAnyHostname(true);
|
||||||
|
identityServiceConfig.setClientKeystore(null);
|
||||||
|
identityServiceConfig.setDisableTrustManager(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateNonExistingUserInRepo()
|
||||||
|
{
|
||||||
|
assertFalse(personService.personExists(IDS_USERNAME));
|
||||||
|
|
||||||
|
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
||||||
|
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
accessTokenAuthorization.getAccessToken().getTokenValue());
|
||||||
|
|
||||||
|
NodeRef person = personService.getPerson(IDS_USERNAME);
|
||||||
|
|
||||||
|
assertTrue(userInfoOptional.isPresent());
|
||||||
|
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
||||||
|
assertEquals("John", userInfoOptional.get().firstName());
|
||||||
|
assertEquals("Doe", userInfoOptional.get().lastName());
|
||||||
|
assertEquals("johndoe@test.com", userInfoOptional.get().email());
|
||||||
|
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
||||||
|
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
||||||
|
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
||||||
|
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCallUserInfoEndpointAndCreateUser() throws IllegalAccessException, NoSuchFieldException
|
||||||
|
{
|
||||||
|
assertFalse(personService.personExists(IDS_USERNAME));
|
||||||
|
|
||||||
|
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
|
||||||
|
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
|
||||||
|
|
||||||
|
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
|
||||||
|
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
|
||||||
|
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
|
||||||
|
when(idsServiceFacadeMock.getUserInfo(accessToken)).thenReturn(identityServiceFacade.getUserInfo(accessToken));
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
declaredField.setAccessible(true);
|
||||||
|
declaredField.set(jitProvisioningHandler, idsServiceFacadeMock);
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
accessToken);
|
||||||
|
|
||||||
|
declaredField.set(jitProvisioningHandler, identityServiceFacade);
|
||||||
|
|
||||||
|
NodeRef person = personService.getPerson(IDS_USERNAME);
|
||||||
|
|
||||||
|
assertTrue(userInfoOptional.isPresent());
|
||||||
|
assertEquals(IDS_USERNAME, userInfoOptional.get().username());
|
||||||
|
assertEquals("John", userInfoOptional.get().firstName());
|
||||||
|
assertEquals("Doe", userInfoOptional.get().lastName());
|
||||||
|
assertEquals("johndoe@test.com", userInfoOptional.get().email());
|
||||||
|
assertEquals(IDS_USERNAME, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
|
||||||
|
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
|
||||||
|
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
|
||||||
|
assertEquals("johndoe@test.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
|
||||||
|
verify(idsServiceFacadeMock).decodeToken(accessToken);
|
||||||
|
verify(idsServiceFacadeMock).getUserInfo(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown()
|
||||||
|
{
|
||||||
|
AuthenticationUtil.runAsSystem(new AuthenticationUtil.RunAsWork<Void>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Void doWork() throws Exception
|
||||||
|
{
|
||||||
|
transactionService.getRetryingTransactionHelper()
|
||||||
|
.doInTransaction((RetryingTransactionCallback<Void>) () -> {
|
||||||
|
personService.deletePerson(IDS_USERNAME);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* #%L
|
||||||
|
* Alfresco Repository
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2005 - 2023 Alfresco Software Limited
|
||||||
|
* %%
|
||||||
|
* This file is part of the Alfresco software.
|
||||||
|
* If the software was purchased under a paid Alfresco license, the terms of
|
||||||
|
* the paid license agreement will prevail. Otherwise, the software is
|
||||||
|
* provided under the following open source license terms:
|
||||||
|
*
|
||||||
|
* Alfresco is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Alfresco is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
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.Mock;
|
||||||
|
|
||||||
|
public class IdentityServiceJITProvisioningHandlerUnitTest
|
||||||
|
{
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IdentityServiceFacade identityServiceFacade;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PersonService personService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IdentityServiceFacade.DecodedAccessToken decodedAccessToken;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TransactionService transactionService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OIDCUserInfo userInfo;
|
||||||
|
|
||||||
|
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
|
||||||
|
|
||||||
|
private static final String JWT_TOKEN = "myToken";
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup()
|
||||||
|
{
|
||||||
|
initMocks(this);
|
||||||
|
|
||||||
|
when(transactionService.isReadOnly()).thenReturn(false);
|
||||||
|
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
|
||||||
|
when(personService.createMissingPeople()).thenReturn(true);
|
||||||
|
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
|
||||||
|
personService, transactionService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExtractUserInfoForExistingUser()
|
||||||
|
{
|
||||||
|
when(personService.personExists("johny123")).thenReturn(true);
|
||||||
|
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("johny123");
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
JWT_TOKEN);
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("johny123", result.get().username());
|
||||||
|
assertFalse(result.get().allFieldsNotEmpty());
|
||||||
|
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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");
|
||||||
|
|
||||||
|
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());
|
||||||
|
assertTrue(result.get().allFieldsNotEmpty());
|
||||||
|
verify(personService).createPerson(any());
|
||||||
|
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
|
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());
|
||||||
|
assertTrue(result.get().allFieldsNotEmpty());
|
||||||
|
verify(personService).createPerson(any());
|
||||||
|
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
|
||||||
|
{
|
||||||
|
|
||||||
|
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
JWT_TOKEN);
|
||||||
|
|
||||||
|
assertFalse(result.isPresent());
|
||||||
|
verify(personService, never()).createPerson(any());
|
||||||
|
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCallUserInfoEndpointToGetUsername()
|
||||||
|
{
|
||||||
|
when(personService.personExists("johny123")).thenReturn(true);
|
||||||
|
|
||||||
|
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
|
||||||
|
|
||||||
|
when(userInfo.username()).thenReturn("johny123");
|
||||||
|
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
|
||||||
|
|
||||||
|
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
|
||||||
|
JWT_TOKEN);
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("johny123", 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotCallUserInfoEndpointIfTokenIsNullOrEmpty()
|
||||||
|
{
|
||||||
|
jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(null);
|
||||||
|
jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded("");
|
||||||
|
|
||||||
|
verify(personService, never()).createPerson(any());
|
||||||
|
verify(identityServiceFacade, never()).decodeToken(null);
|
||||||
|
verify(identityServiceFacade, never()).decodeToken("");
|
||||||
|
verify(identityServiceFacade, never()).getUserInfo(null);
|
||||||
|
verify(identityServiceFacade, never()).getUserInfo("");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -25,9 +25,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
|
||||||
|
|
||||||
import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceRemoteUserMapper.USERNAME_CLAIM;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -38,13 +35,15 @@ import java.util.Map;
|
|||||||
import java.util.Vector;
|
import java.util.Vector;
|
||||||
import java.util.function.Supplier;
|
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 junit.framework.TestCase;
|
||||||
import org.alfresco.repo.security.authentication.AuthenticationException;
|
import org.alfresco.repo.security.authentication.AuthenticationException;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.DecodedAccessToken;
|
||||||
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
|
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.TokenDecodingException;
|
||||||
import org.alfresco.service.cmr.security.PersonService;
|
import org.alfresco.service.cmr.security.PersonService;
|
||||||
|
import org.alfresco.service.transaction.TransactionService;
|
||||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,16 +91,19 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
|||||||
|
|
||||||
private IdentityServiceRemoteUserMapper givenMapper(Map<String, Supplier<String>> tokenToUser)
|
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);
|
||||||
|
final PersonService personService = mock(PersonService.class);
|
||||||
|
when(transactionService.isReadOnly()).thenReturn(true);
|
||||||
when(facade.decodeToken(anyString()))
|
when(facade.decodeToken(anyString()))
|
||||||
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
|
.thenAnswer(i -> new TestDecodedToken(tokenToUser.get(i.getArgument(0, String.class))));
|
||||||
|
|
||||||
final PersonService personService = mock(PersonService.class);
|
|
||||||
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
when(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
|
||||||
|
|
||||||
|
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService);
|
||||||
|
|
||||||
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
|
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
|
||||||
mapper.setIdentityServiceFacade(facade);
|
mapper.setJitProvisioningHandler(jitProvisioning);
|
||||||
mapper.setPersonService(personService);
|
|
||||||
mapper.setActive(true);
|
mapper.setActive(true);
|
||||||
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
|
mapper.setBearerTokenResolver(new DefaultBearerTokenResolver());
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
|
|||||||
@Override
|
@Override
|
||||||
public Object getClaim(String claim)
|
public Object getClaim(String claim)
|
||||||
{
|
{
|
||||||
return USERNAME_CLAIM.equals(claim) ? usernameSupplier.get() : null;
|
return PersonClaims.PREFERRED_USERNAME_CLAIM_NAME.equals(claim) ? usernameSupplier.get() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.alfresco.repo.security.authentication.identityservice;
|
package org.alfresco.repo.security.authentication.identityservice;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -73,11 +74,24 @@ public class SpringBasedIdentityServiceFacadeUnitTest
|
|||||||
.havingCause().withNoCause().withMessage("Expected");
|
.havingCause().withNoCause().withMessage("Expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnEmptyOptionalOnFailure()
|
||||||
|
{
|
||||||
|
final RestOperations restOperations = mock(RestOperations.class);
|
||||||
|
final JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
||||||
|
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(facade.getUserInfo(TOKEN).isEmpty()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
private ClientRegistration testRegistration()
|
private ClientRegistration testRegistration()
|
||||||
{
|
{
|
||||||
return ClientRegistration.withRegistrationId("test")
|
return ClientRegistration.withRegistrationId("test")
|
||||||
.tokenUri("http://localhost")
|
.tokenUri("http://localhost")
|
||||||
.clientId("test")
|
.clientId("test")
|
||||||
|
.userInfoUri("http://localhost/userinfo")
|
||||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,8 @@
|
|||||||
# Test identity service authentication overrides
|
# Test identity service authentication overrides
|
||||||
#identity-service.auth-server-url=http://192.168.0.1:8180/auth
|
#identity-service.auth-server-url=http://192.168.0.1:8180/auth
|
||||||
identity-service.realm=alfresco
|
identity-service.realm=alfresco
|
||||||
identity-service.realm-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWLQxipXNe6cLnVPGy7l\
|
|
||||||
BgyR51bDiK7Jso8Rmh2TB+bmO4fNaMY1ETsxECSM0f6NTV0QHks9+gBe+pB6JNeM\
|
|
||||||
uPmaE/M/MsE9KUif9L2ChFq3zor6s2foFv2DTiTkij+1aQF9fuIjDNH4FC6L252W\
|
|
||||||
ydZzh+f73Xuy5evdPj+wrPYqWyP7sKd+4Q9EIILWAuTDvKEjwyZmIyfM/nUn6ltD\
|
|
||||||
P6W8xMP0PoEJNAAp79anz2jk2HP2PvC2qdjVsphdTk3JG5qQMB0WJUh4Kjgabd4j\
|
|
||||||
QJ77U8gTRswKgNHRRPWhruiIcmmkP+zI0ozNW6rxH3PF4L7M9rXmfcmUcBcKf+Yx\
|
|
||||||
jwIDAQAB
|
|
||||||
identity-service.ssl-required=external
|
identity-service.ssl-required=external
|
||||||
identity-service.resource=test
|
identity-service.resource=alfresco
|
||||||
identity-service.public-client=false
|
identity-service.public-client=false
|
||||||
identity-service.confidential-port=100
|
identity-service.confidential-port=100
|
||||||
identity-service.use-resource-role-mappings=true
|
identity-service.use-resource-role-mappings=true
|
||||||
@@ -41,11 +34,11 @@ identity-service.min-time-between-jwks-requests=60
|
|||||||
identity-service.public-key-cache-ttl=3600
|
identity-service.public-key-cache-ttl=3600
|
||||||
identity-service.enable-pkce=true
|
identity-service.enable-pkce=true
|
||||||
identity-service.ignore-oauth-query-parameter=true
|
identity-service.ignore-oauth-query-parameter=true
|
||||||
identity-service.credentials.secret=11111
|
identity-service.credentials.secret=
|
||||||
identity-service.credentials.provider=secret
|
identity-service.credentials.provider=secret
|
||||||
identity-service.client-socket-timeout=1000
|
identity-service.client-socket-timeout=1000
|
||||||
identity-service.client-connection-timeout=3000
|
identity-service.client-connection-timeout=3000
|
||||||
identity-service.authentication.enable-username-password-authentication=false
|
identity-service.authentication.enable-username-password-authentication=true
|
||||||
|
|
||||||
# Use a date in the past, so data is read straight away rather than being scheduled in tests. A few ms is too late.
|
# Use a date in the past, so data is read straight away rather than being scheduled in tests. A few ms is too late.
|
||||||
mimetype.config.cronExpression=0 0 0 ? JAN * 1970
|
mimetype.config.cronExpression=0 0 0 ? JAN * 1970
|
||||||
|
@@ -1866,6 +1866,46 @@
|
|||||||
],
|
],
|
||||||
"totp": false,
|
"totp": false,
|
||||||
"username": "testuser"
|
"username": "testuser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientRoles": {
|
||||||
|
"account": [
|
||||||
|
"manage-account",
|
||||||
|
"view-profile"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"credentials": [
|
||||||
|
{
|
||||||
|
"algorithm": "pbkdf2",
|
||||||
|
"counter": 0,
|
||||||
|
"digits": 0,
|
||||||
|
"hashIterations": 20000,
|
||||||
|
"hashedSaltedValue": "+A2UrlK6T33IHVutjQj9k8S8kMco1IMnmCTngEg+PE+2vO4jJScux6wcltsRIYILv5ggcS3PI7tbsynq5u39sQ==",
|
||||||
|
"period": 0,
|
||||||
|
"salt": "IyVlItIo27bmACSLi4yQkA==",
|
||||||
|
"type": "password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disableableCredentialTypes": [
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"email": "johndoe@test.com",
|
||||||
|
"emailVerified": false,
|
||||||
|
"enabled": true,
|
||||||
|
"firstName": "John",
|
||||||
|
"groups": [
|
||||||
|
"/admin",
|
||||||
|
"/testgroup"
|
||||||
|
],
|
||||||
|
"lastName": "Doe",
|
||||||
|
"realmRoles": [
|
||||||
|
"uma_authorization",
|
||||||
|
"user",
|
||||||
|
"offline_access",
|
||||||
|
"test_role"
|
||||||
|
],
|
||||||
|
"totp": false,
|
||||||
|
"username": "johndoe123"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keycloakVersion": "8.0.1",
|
"keycloakVersion": "8.0.1",
|
||||||
|
@@ -10,7 +10,7 @@ services:
|
|||||||
- "8090:8090"
|
- "8090:8090"
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15.4
|
image: postgres:15.4
|
||||||
profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio"]
|
profiles: ["default", "with-transform-core-aio", "postgres", "with-mtls-transform-core-aio", "with-ids"]
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=alfresco
|
- POSTGRES_PASSWORD=alfresco
|
||||||
- POSTGRES_USER=alfresco
|
- POSTGRES_USER=alfresco
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
activemq:
|
activemq:
|
||||||
profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio"]
|
profiles: ["default", "with-transform-core-aio", "activemq", "with-mtls-transform-core-aio", "with-ids"]
|
||||||
image: alfresco/alfresco-activemq:5.18.3-jre17-rockylinux8
|
image: alfresco/alfresco-activemq:5.18.3-jre17-rockylinux8
|
||||||
ports:
|
ports:
|
||||||
- "5672:5672" # AMQP
|
- "5672:5672" # AMQP
|
||||||
@@ -57,3 +57,15 @@ services:
|
|||||||
CLIENT_SSL_TRUST_STORE: "file:/tengineAIO.truststore"
|
CLIENT_SSL_TRUST_STORE: "file:/tengineAIO.truststore"
|
||||||
CLIENT_SSL_TRUST_STORE_PASSWORD: "password"
|
CLIENT_SSL_TRUST_STORE_PASSWORD: "password"
|
||||||
CLIENT_SSL_TRUST_STORE_TYPE: "JCEKS"
|
CLIENT_SSL_TRUST_STORE_TYPE: "JCEKS"
|
||||||
|
alfresco-identity-service:
|
||||||
|
profiles: ["with-ids"]
|
||||||
|
image: quay.io/alfresco/alfresco-identity-service:2.0.0
|
||||||
|
environment:
|
||||||
|
- KEYCLOAK_ADMIN=admin
|
||||||
|
- KEYCLOAK_ADMIN_PASSWORD=admin
|
||||||
|
- KC_DB=dev-mem
|
||||||
|
command: ["start-dev", "--import-realm", "--http-relative-path=/auth", "--hostname=localhost", "--http-enabled=true"]
|
||||||
|
volumes:
|
||||||
|
- ../../../repository/src/test/resources/realms/alfresco-realm.json:/opt/keycloak/data/import/alfresco-realm.json
|
||||||
|
ports:
|
||||||
|
- 8999:8080
|
@@ -1,15 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
echo "=========================== Starting AppContext05TestSuite setup ==========================="
|
|
||||||
PS4="\[\e[35m\]+ \[\e[m\]"
|
|
||||||
set -vex
|
|
||||||
pushd "$(dirname "${BASH_SOURCE[0]}")/../../../"
|
|
||||||
|
|
||||||
mkdir -p "${HOME}/tmp"
|
|
||||||
cp repository/src/test/resources/realms/alfresco-realm.json "${HOME}/tmp"
|
|
||||||
echo "HOST_IP=$(hostname -I | cut -f1 -d' ')" >> $GITHUB_ENV
|
|
||||||
docker run -d -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_VENDOR=h2 -p 8999:8080 -e KEYCLOAK_IMPORT=/tmp/alfresco-realm.json -v $HOME/tmp/alfresco-realm.json:/tmp/alfresco-realm.json alfresco/alfresco-identity-service:1.2
|
|
||||||
|
|
||||||
popd
|
|
||||||
set +vex
|
|
||||||
echo "=========================== Finishing AppContext05TestSuite setup =========================="
|
|
Reference in New Issue
Block a user