ACS-6601 Implement Repository OIDC Compliance (#2447)

* ACS-6677 Enhance OIDC Configuration Flexibility (#2426)

* ACS-6603 Implement OIDC Compliance (#2442)

* ACS-6677 Enhance OIDC Configuration Flexibility

* ACS-6677 Revert changing http header

* ACS-6677 Add unit test to suite

* ACS-6677 Rename var

* ACS-6677 Fix PMD issues

* ACS-6677 Fix PMD issues

* ACS-6677 Improve code

* ACS-6677 Fix compatibility

* ACS-6677 Add JwtAudienceValidator

* ACS-6677 Change domain

* ACS-6603 Oidc compliance

* ACS-6603 Add Auth0 test

* ACS-6603 Reformat

* ACS-6603 Enable User Info Endpoint test + Refactor

* ACS-6603 Change test condition

* ACS-6603 Add state parameter + reformat stream

* ACS-6603 Use enum type
This commit is contained in:
Damian Ujma
2024-02-13 18:43:44 +01:00
committed by GitHub
parent de6b062f3e
commit c4714b19eb
20 changed files with 1121 additions and 221 deletions

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,7 @@
*/
package org.alfresco;
import org.alfresco.repo.security.authentication.identityservice.ClientRegistrationProviderUnitTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBeanTest;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceJITProvisioningHandlerUnitTest;
import org.alfresco.repo.security.authentication.identityservice.LazyInstantiatingIdentityServiceFacadeUnitTest;
@@ -151,6 +152,7 @@ import org.junit.runners.Suite;
AdminConsoleAuthenticationCookiesServiceUnitTest.class,
AdminConsoleHttpServletRequestWrapperUnitTest.class,
IdentityServiceAdminConsoleAuthenticatorUnitTest.class,
ClientRegistrationProviderUnitTest.class,
org.alfresco.repo.security.authentication.CompositePasswordEncoderTest.class,
org.alfresco.repo.security.authentication.PasswordHashingTest.class,
org.alfresco.repo.security.authority.script.ScriptAuthorityService_RegExTest.class,

View File

@@ -0,0 +1,266 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2024 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.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import java.util.Set;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.ClientRegistrationProvider;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.web.client.RestTemplate;
public class ClientRegistrationProviderUnitTest
{
private static final String CLIENT_ID = "alfresco";
private static final String OPENID_CONFIGURATION = "{\"token_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/token\",\"token_endpoint_auth_methods_supported\":[\"client_secret_post\",\"private_key_jwt\",\"client_secret_basic\"],\"jwks_uri\":\"https://login.serviceonline.alfresco/common/discovery/v2.0/keys\",\"response_modes_supported\":[\"query\",\"fragment\",\"form_post\"],\"subject_types_supported\":[\"pairwise\"],\"id_token_signing_alg_values_supported\":[\"RS256\"],\"response_types_supported\":[\"code\",\"id_token\",\"code id_token\",\"id_token token\"],\"scopes_supported\":[\"openid\",\"profile\",\"email\",\"offline_access\"],\"issuer\":\"https://login.serviceonline.alfresco/alfresco/v2.0\",\"request_uri_parameter_supported\":false,\"userinfo_endpoint\":\"https://graph.service.alfresco/oidc/userinfo\",\"authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/authorize\",\"device_authorization_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/devicecode\",\"http_logout_supported\":true,\"frontchannel_logout_supported\":true,\"end_session_endpoint\":\"https://login.serviceonline.alfresco/common/oauth2/v2.0/logout\",\"claims_supported\":[\"sub\",\"iss\",\"cloud_instance_name\",\"cloud_instance_host_name\",\"cloud_graph_host_name\",\"msgraph_host\",\"aud\",\"exp\",\"iat\",\"auth_time\",\"acr\",\"nonce\",\"preferred_username\",\"name\",\"tid\",\"ver\",\"at_hash\",\"c_hash\",\"email\"],\"kerberos_endpoint\":\"https://login.serviceonline.alfresco/common/kerberos\",\"tenant_region_scope\":null,\"cloud_instance_name\":\"serviceonline.alfresco\",\"cloud_graph_host_name\":\"graph.oidc.net\",\"msgraph_host\":\"graph.service.alfresco\",\"rbac_url\":\"https://pas.oidc.alfresco\"}";
private static final String DISCOVERY_PATH_SEGMENTS = "/.well-known/openid-configuration";
private static final String AUTH_SERVER = "https://login.serviceonline.alfresco";
private IdentityServiceConfig config;
private RestTemplate restTemplate;
private OIDCProviderMetadata oidcResponse;
private ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
@Before
public void setup() throws ParseException
{
config = new IdentityServiceConfig();
config.setAuthServerUrl(AUTH_SERVER);
config.setResource(CLIENT_ID);
restTemplate = mock(RestTemplate.class);
ResponseEntity responseEntity = mock(ResponseEntity.class);
when(restTemplate.exchange(requestEntityCaptor.capture(), eq(String.class))).thenReturn(responseEntity);
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
when(responseEntity.hasBody()).thenReturn(true);
when(responseEntity.getBody()).thenReturn("");
oidcResponse = spy(OIDCProviderMetadata.parse(OPENID_CONFIGURATION));
}
@Test
public void shouldCreateClientRegistration()
{
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
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).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getTokenUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldCreateClientRegistrationWithoutIssuerConfigured()
{
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).isNotNull();
assertThat(clientRegistration.getClientId()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getTokenUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint()).isNotNull();
assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNotNull();
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfIssuerIsNotValid()
{
config.setIssuerUrl("https://invalidissuer.alfresco");
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfIssuerIsNull()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getIssuer()).thenReturn(null);
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfTokenEndpointIsNull()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getTokenEndpointURI()).thenReturn(null);
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfAuthorizationEndpointIsNull()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getAuthorizationEndpointURI()).thenReturn(null);
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfUserInfoEndpointIsNull()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getUserInfoEndpointURI()).thenReturn(null);
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldThrowIdentityServiceExceptionIfJWKSetEndpointIsNull()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getJWKSetURI()).thenReturn(null);
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
assertThrows(IdentityServiceException.class,
() -> new ClientRegistrationProvider(config).createClientRegistration(restTemplate));
}
}
@Test
public void shouldCreateDiscoveryEndpointWithRealm()
{
config.setRealm("alfresco");
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
AUTH_SERVER + "/realms/alfresco" + DISCOVERY_PATH_SEGMENTS);
}
}
@Test
public void shouldSetAllSupportedScopes()
{
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.getScopes().containsAll(
Set.of("openid", "profile", "email"))).isTrue();
}
}
@Test
public void shouldSetOneSupportedScope()
{
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
when(oidcResponse.getScopes()).thenReturn(new Scope("openid"));
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
restTemplate);
assertThat(clientRegistration.getScopes().size()).isEqualTo(1);
assertThat(clientRegistration.getScopes().stream().findFirst().get()).isEqualTo("openid");
}
}
@Test
public void shouldCreateDiscoveryEndpointFromIssuer()
{
config.setAuthServerUrl(null);
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
{
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
new ClientRegistrationProvider(config).createClientRegistration(restTemplate);
assertThat(requestEntityCaptor.getValue().getUrl().toASCIIString()).isEqualTo(
"https://login.serviceonline.alfresco/alfresco/v2.0" + DISCOVERY_PATH_SEGMENTS);
}
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -29,10 +29,14 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtAudienceValidator;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtDecoderProvider;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacadeFactoryBean.JwtIssuerValidator;
import org.junit.Test;
@@ -45,11 +49,14 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
public class IdentityServiceFacadeFactoryBeanTest
{
private static final String EXPECTED_ISSUER = "expected-issuer";
private static final String EXPECTED_AUDIENCE = "expected-audience";
@Test
public void shouldCreateJwtDecoderWithoutIDSWhenPublicKeyIsProvided()
{
final IdentityServiceConfig config = mock(IdentityServiceConfig.class);
when(config.getRealmKey()).thenReturn("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAve3MabX/rp3LbE7/zNqKxuid8WT7y4qSXsNaiPvl/OVbNWW/cu5td1VndItYhH6/gL7Z5W/r4MOeTlz/fOdXfjrRJou2f3UiPQwLV9RdOH3oS4/BUe+sviD8Q3eRfWBWWz3yw8f2YNtD4bMztIMMjqthvwdEEb9S9jbxxD0o71Bsrz/FwPi7HhSDA+Z/p01Hct8m4wx13ZlKRd4YjyC12FBmi9MSgsrFuWzyQHhHTeBDoALpfuiut3rhVxUtFmVTpy6p9vil7C5J5pok4MXPH0dJCyDNQz05ww5+fD+tfksIEpFeokRpN226F+P21oQVFUWwYIaXaFlG/hfvwmnlfQIDAQAB");
when(config.isClientIdValidationDisabled()).thenReturn(true);
final ProviderDetails providerDetails = mock(ProviderDetails.class);
when(providerDetails.getIssuerUri()).thenReturn("https://my.issuer");
@@ -108,12 +115,78 @@ public class IdentityServiceFacadeFactoryBeanTest
assertThat(validationResult.getErrors()).isEmpty();
}
@Test
public void shouldFailWithNotMatchingAudienceList()
{
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(
tokenWithAudience(List.of("different-audience")));
assertThat(validationResult).isNotNull();
assertThat(validationResult.hasErrors()).isTrue();
assertThat(validationResult.getErrors()).hasSize(1);
final OAuth2Error error = validationResult.getErrors().iterator().next();
assertThat(error).isNotNull();
assertThat(error.getDescription()).contains(EXPECTED_AUDIENCE);
}
@Test
public void shouldFailWithNullAudience()
{
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(tokenWithAudience(null));
assertThat(validationResult).isNotNull();
assertThat(validationResult.hasErrors()).isTrue();
assertThat(validationResult.getErrors()).hasSize(1);
final OAuth2Error error = validationResult.getErrors().iterator().next();
assertThat(error).isNotNull();
assertThat(error.getDescription()).contains(EXPECTED_AUDIENCE);
}
@Test
public void shouldSucceedWithMatchingAudienceList()
{
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(
tokenWithAudience(List.of(EXPECTED_AUDIENCE)));
assertThat(validationResult).isNotNull();
assertThat(validationResult.hasErrors()).isFalse();
assertThat(validationResult.getErrors()).isEmpty();
}
@Test
public void shouldSucceedWithMatchingSingleAudience()
{
final JwtAudienceValidator audienceValidator = new JwtAudienceValidator(EXPECTED_AUDIENCE);
final Jwt token = Jwt.withTokenValue(UUID.randomUUID().toString())
.claim("aud", EXPECTED_AUDIENCE)
.header("JUST", "FOR TESTING")
.build();
final OAuth2TokenValidatorResult validationResult = audienceValidator.validate(token);
assertThat(validationResult).isNotNull();
assertThat(validationResult.hasErrors()).isFalse();
assertThat(validationResult.getErrors()).isEmpty();
}
private Jwt tokenWithIssuer(String issuer)
{
return Jwt.withTokenValue(UUID.randomUUID().toString())
.issuer(issuer)
.header("JUST", "FOR TESTING")
.build();
.issuer(issuer)
.header("JUST", "FOR TESTING")
.build();
}
private Jwt tokenWithAudience(Collection<String> audience)
{
return Jwt.withTokenValue(UUID.randomUUID().toString())
.audience(audience)
.header("JUST", "FOR TESTING")
.build();
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -25,6 +25,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;
@@ -32,6 +33,8 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Optional;
import com.nimbusds.openid.connect.sdk.claims.PersonClaims;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
import org.alfresco.repo.management.subsystems.DefaultChildApplicationContextManager;
@@ -57,6 +60,14 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
private IdentityServiceFacade identityServiceFacade;
private IdentityServiceJITProvisioningHandler jitProvisioningHandler;
private final boolean isAuth0Enabled = Optional.ofNullable(System.getProperty("auth0.enabled"))
.map(Boolean::valueOf)
.orElse(false);
private final String userPassword = Optional.ofNullable(System.getProperty("admin.password"))
.filter(password -> isAuth0Enabled)
.orElse("password");
@Before
public void setup()
{
@@ -85,7 +96,8 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
assertFalse(personService.personExists(IDS_USERNAME));
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
Optional<OIDCUserInfo> userInfoOptional = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
accessTokenAuthorization.getAccessToken().getTokenValue());
@@ -94,13 +106,17 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
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("johndoe123@alfresco.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));
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());
assertEquals("Doe", userInfoOptional.get().lastName());
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
}
}
@Test
@@ -108,13 +124,15 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
{
assertFalse(personService.personExists(IDS_USERNAME));
String principalAttribute = isAuth0Enabled ? PersonClaims.NICKNAME_CLAIM_NAME : PersonClaims.PREFERRED_USERNAME_CLAIM_NAME;
IdentityServiceFacade.AccessTokenAuthorization accessTokenAuthorization =
identityServiceFacade.authorize(IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, "password"));
identityServiceFacade.authorize(
IdentityServiceFacade.AuthorizationGrant.password(IDS_USERNAME, userPassword));
String accessToken = accessTokenAuthorization.getAccessToken().getTokenValue();
IdentityServiceFacade idsServiceFacadeMock = mock(IdentityServiceFacade.class);
when(idsServiceFacadeMock.decodeToken(accessToken)).thenReturn(null);
when(idsServiceFacadeMock.getUserInfo(accessToken)).thenReturn(identityServiceFacade.getUserInfo(accessToken));
when(idsServiceFacadeMock.getUserInfo(accessToken, principalAttribute)).thenReturn(identityServiceFacade.getUserInfo(accessToken, principalAttribute));
// Replace the original facade with a mocked one to prevent user information from being extracted from the access token.
Field declaredField = jitProvisioningHandler.getClass()
@@ -131,15 +149,18 @@ public class IdentityServiceJITProvisioningHandlerTest extends BaseSpringTest
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));
assertEquals("johndoe123@alfresco.com", userInfoOptional.get().email());
assertEquals("johndoe123@alfresco.com", nodeService.getProperty(person, ContentModel.PROP_EMAIL));
verify(idsServiceFacadeMock).decodeToken(accessToken);
verify(idsServiceFacadeMock).getUserInfo(accessToken);
verify(idsServiceFacadeMock, atLeast(1)).getUserInfo(accessToken, principalAttribute);
if (!isAuth0Enabled)
{
assertEquals("John", userInfoOptional.get().firstName());
assertEquals("Doe", userInfoOptional.get().lastName());
assertEquals("John", nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME));
assertEquals("Doe", nodeService.getProperty(person, ContentModel.PROP_LASTNAME));
}
}
@After

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -60,6 +60,9 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
@Mock
private TransactionService transactionService;
@Mock
private IdentityServiceConfig identityServiceConfig;
@Mock
private OIDCUserInfo userInfo;
@@ -76,7 +79,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(identityServiceFacade.decodeToken(JWT_TOKEN)).thenReturn(decodedAccessToken);
when(personService.createMissingPeople()).thenReturn(true);
jitProvisioningHandler = new IdentityServiceJITProvisioningHandler(identityServiceFacade,
personService, transactionService);
personService, transactionService, identityServiceConfig);
}
@Test
@@ -91,7 +94,23 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
assertTrue(result.isPresent());
assertEquals("johny123", result.get().username());
assertFalse(result.get().allFieldsNotEmpty());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldExtractUserInfoForExistingUserWithProviderPrincipalAttribute()
{
when(identityServiceConfig.getPrincipalAttribute()).thenReturn("nickname");
when(personService.personExists("johny123")).thenReturn(true);
when(decodedAccessToken.getClaim("nickname")).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, "nickname");
}
@Test
@@ -114,7 +133,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
assertEquals("johny123@email.com", result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN);
verify(identityServiceFacade, never()).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
@@ -128,7 +147,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
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));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
@@ -140,21 +159,21 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
assertEquals("johny123@email.com", result.get().email());
assertTrue(result.get().allFieldsNotEmpty());
verify(personService).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
public void shouldReturnEmptyOptionalIfUsernameNotExtracted()
{
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
assertFalse(result.isPresent());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@Test
@@ -165,7 +184,7 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
when(decodedAccessToken.getClaim(PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn("");
when(userInfo.username()).thenReturn("johny123");
when(identityServiceFacade.getUserInfo(JWT_TOKEN)).thenReturn(Optional.of(userInfo));
when(identityServiceFacade.getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME)).thenReturn(Optional.of(userInfo));
Optional<OIDCUserInfo> result = jitProvisioningHandler.extractUserInfoAndCreateUserIfNeeded(
JWT_TOKEN);
@@ -177,7 +196,31 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
assertEquals("", result.get().email());
assertFalse(result.get().allFieldsNotEmpty());
verify(personService, never()).createPerson(any());
verify(identityServiceFacade).getUserInfo(JWT_TOKEN);
verify(identityServiceFacade).getUserInfo(JWT_TOKEN, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
@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));
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, "nickname");
}
@Test
@@ -189,8 +232,8 @@ public class IdentityServiceJITProvisioningHandlerUnitTest
verify(personService, never()).createPerson(any());
verify(identityServiceFacade, never()).decodeToken(null);
verify(identityServiceFacade, never()).decodeToken("");
verify(identityServiceFacade, never()).getUserInfo(null);
verify(identityServiceFacade, never()).getUserInfo("");
verify(identityServiceFacade, never()).getUserInfo(null, PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
verify(identityServiceFacade, never()).getUserInfo("", PersonClaims.PREFERRED_USERNAME_CLAIM_NAME);
}
}

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -94,13 +94,14 @@ public class IdentityServiceRemoteUserMapperTest extends TestCase
final TransactionService transactionService = mock(TransactionService.class);
final IdentityServiceFacade facade = mock(IdentityServiceFacade.class);
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(personService.getUserIdentifier(anyString())).thenAnswer(i -> i.getArgument(0, String.class));
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService);
final IdentityServiceJITProvisioningHandler jitProvisioning = new IdentityServiceJITProvisioningHandler(facade, personService, transactionService, identityServiceConfig);
final IdentityServiceRemoteUserMapper mapper = new IdentityServiceRemoteUserMapper();
mapper.setJitProvisioningHandler(jitProvisioning);

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -83,7 +83,7 @@ public class SpringBasedIdentityServiceFacadeUnitTest
final SpringBasedIdentityServiceFacade facade = new SpringBasedIdentityServiceFacade(restOperations, testRegistration(), jwtDecoder);
assertThat(facade.getUserInfo(TOKEN).isEmpty()).isTrue();
assertThat(facade.getUserInfo(TOKEN, "preferred_username").isEmpty()).isTrue();
}
private ClientRegistration testRegistration()

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2024 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -36,10 +36,15 @@ import static org.mockito.MockitoAnnotations.initMocks;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import com.nimbusds.oauth2.sdk.Scope;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.alfresco.repo.security.authentication.external.RemoteUserMapper;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceConfig;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessToken;
import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.AccessTokenAuthorization;
@@ -68,6 +73,8 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Mock
IdentityServiceFacade identityServiceFacade;
@Mock
IdentityServiceConfig identityServiceConfig;
@Mock
AdminConsoleAuthenticationCookiesService cookiesService;
@Mock
RemoteUserMapper remoteUserMapper;
@@ -88,9 +95,12 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
initMocks(this);
ClientRegistration clientRegistration = mock(ClientRegistration.class);
ProviderDetails providerDetails = mock(ProviderDetails.class);
Scope scope = Scope.parse(Arrays.asList("openid", "profile", "email", "offline_access"));
when(clientRegistration.getProviderDetails()).thenReturn(providerDetails);
when(clientRegistration.getClientId()).thenReturn("alfresco");
when(providerDetails.getAuthorizationUri()).thenReturn("http://localhost:8999/auth");
when(providerDetails.getConfigurationMetadata()).thenReturn(Map.of("scopes_supported", scope));
when(identityServiceFacade.getClientRegistration()).thenReturn(clientRegistration);
when(request.getRequestURL()).thenReturn(adminConsoleURL);
when(remoteUserMapper.getRemoteUser(request)).thenReturn(null);
@@ -100,6 +110,7 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
authenticator.setIdentityServiceFacade(identityServiceFacade);
authenticator.setCookiesService(cookiesService);
authenticator.setRemoteUserMapper(remoteUserMapper);
authenticator.setIdentityServiceConfig(identityServiceConfig);
}
@Test
@@ -142,11 +153,45 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
@Test
public void shouldCallAuthChallenge() throws IOException
{
String authenticationRequest = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=" + adminConsoleURL
+ "&response_type=code&scope=openid";
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
.formatted("http://localhost:8080", redirectPath);
authenticator.requestAuthentication(request, response);
verify(response).sendRedirect(authenticationRequest);
verify(response).sendRedirect(authenticationRequest.capture());
assertTrue(authenticationRequest.getValue().contains(expectedUri));
assertTrue(authenticationRequest.getValue().contains("openid"));
assertTrue(authenticationRequest.getValue().contains("profile"));
assertTrue(authenticationRequest.getValue().contains("email"));
assertTrue(authenticationRequest.getValue().contains("offline_access"));
assertTrue(authenticationRequest.getValue().contains("state"));
}
@Test
public void shouldCallAuthChallengeWithAudience() throws IOException
{
String audience = "http://localhost:8082";
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
when(identityServiceConfig.getAudience()).thenReturn(audience);
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(redirectPath);
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);
authenticator.requestAuthentication(request, response);
verify(response).sendRedirect(authenticationRequest.capture());
assertTrue(authenticationRequest.getValue().contains(expectedUri));
assertTrue(authenticationRequest.getValue().contains("openid"));
assertTrue(authenticationRequest.getValue().contains("profile"));
assertTrue(authenticationRequest.getValue().contains("email"));
assertTrue(authenticationRequest.getValue().contains("offline_access"));
assertTrue(authenticationRequest.getValue().contains("audience=%s".formatted(audience)));
assertTrue(authenticationRequest.getValue().contains("state"));
}
@Test

View File

@@ -1889,7 +1889,7 @@
"disableableCredentialTypes": [
"password"
],
"email": "johndoe@test.com",
"email": "johndoe123@alfresco.com",
"emailVerified": false,
"enabled": true,
"firstName": "John",