mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2026-09-16 18:13:17 +00:00
ACS-11311 Authentication setup for Microsoft Entra (#3989)
This commit is contained in:
+12
-1
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -78,6 +78,17 @@ public class IdentityServiceConfig
|
||||
private long jwtClockSkewMs;
|
||||
private String webScriptsHomeRedirectPath;
|
||||
private String webScriptsHomeScopes;
|
||||
private boolean scopeValidationDisabled;
|
||||
|
||||
public boolean isScopeValidationDisabled()
|
||||
{
|
||||
return scopeValidationDisabled;
|
||||
}
|
||||
|
||||
public void setScopeValidationDisabled(boolean scopeValidationDisabled)
|
||||
{
|
||||
this.scopeValidationDisabled = scopeValidationDisabled;
|
||||
}
|
||||
|
||||
public String getWebScriptsHomeRedirectPath()
|
||||
{
|
||||
|
||||
+7
-1
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -510,6 +510,12 @@ public class IdentityServiceFacadeFactoryBean implements FactoryBean<IdentitySer
|
||||
|
||||
private Set<String> getSupportedScopes(Scope scopes)
|
||||
{
|
||||
if (config.isScopeValidationDisabled())
|
||||
{
|
||||
// Bypass scope filtering against the IDP's scopes_supported discovery metadata.
|
||||
// Required for IDPs like MS Entra that do not advertise custom API scopes
|
||||
return config.getPasswordGrantScopes();
|
||||
}
|
||||
return scopes.stream()
|
||||
.filter(this::hasPasswordGrantScope)
|
||||
.map(Identifier::getValue)
|
||||
|
||||
+6
-1
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -156,6 +156,11 @@ public abstract class AbstractIdentityServiceAuthenticator implements ExternalUs
|
||||
|
||||
private Set<String> getConfiguredScopes(ClientRegistration clientRegistration)
|
||||
{
|
||||
if (identityServiceConfig.isScopeValidationDisabled())
|
||||
{
|
||||
// Bypass filtering: send configured scopes as-is
|
||||
return getConfiguredScopes();
|
||||
}
|
||||
return Optional.ofNullable(clientRegistration.getProviderDetails())
|
||||
.map(ProviderDetails::getConfigurationMetadata)
|
||||
.map(metadata -> metadata.get(SCOPES_SUPPORTED.getValue()))
|
||||
|
||||
+3
@@ -189,6 +189,9 @@
|
||||
<property name="webScriptsHomeRedirectPath">
|
||||
<value>${identity-service.webscripts-home.redirect-path}</value>
|
||||
</property>
|
||||
<property name="scopeValidationDisabled">
|
||||
<value>${identity-service.scope-validation.disabled:false}</value>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<!-- Enable control over mapping between request and user ID -->
|
||||
|
||||
+8
@@ -22,3 +22,11 @@ identity-service.webscripts-home.scopes=openid,profile,email,offline_access
|
||||
identity-service.password-grant.scopes=openid,profile,email
|
||||
identity-service.issuer-attribute=issuer
|
||||
identity-service.jwt-clock-skew-ms=0
|
||||
|
||||
# Controls whether configured OAuth2 scopes are filtered against the IDP's scopes_supported
|
||||
# from the OIDC discovery document before being included in authorization requests. (default - False)
|
||||
#
|
||||
# When set to 'true': Scope filtering is bypassed, all configured scopes are sent
|
||||
# as-is without validation against the IDP's discovery document. This is required for
|
||||
# IDPs like Microsoft Entra (Azure AD) that do not advertise custom API scopes.
|
||||
identity-service.scope-validation.disabled=false
|
||||
+46
-1
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 Alfresco Software Limited
|
||||
* %%
|
||||
* This file is part of the Alfresco software.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
@@ -61,6 +61,7 @@ public class ClientRegistrationProviderUnitTest
|
||||
private static final String ADMIN_CONSOLE_SCOPES = "openid,email,profile,offline_access";
|
||||
private static final String PSSWD_GRANT_SCOPES = "openid,email,profile";
|
||||
private static final String ISSUER_ATRR = "issuer";
|
||||
private static final String CUSTOM_API_SCOPE = "api://client-id/alf_access";
|
||||
|
||||
private IdentityServiceConfig config;
|
||||
private RestTemplate restTemplate;
|
||||
@@ -302,6 +303,50 @@ public class ClientRegistrationProviderUnitTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBypassScopeFilteringWhenScopeValidationDisabled()
|
||||
{
|
||||
// Configure: scope validation disabled, custom API scope included
|
||||
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
|
||||
config.setScopeValidationDisabled(true);
|
||||
config.setPasswordGrantScopes("openid,profile,email," + CUSTOM_API_SCOPE);
|
||||
|
||||
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||
{
|
||||
// Simulate: scopes_supported does NOT include custom API scope
|
||||
when(oidcResponse.getScopes()).thenReturn(new Scope("openid", "profile", "email", "offline_access"));
|
||||
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||
|
||||
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||
restTemplate);
|
||||
|
||||
// Custom API scope should be included when scope validation is disabled
|
||||
assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("openid", "profile", "email", CUSTOM_API_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFilterCustomApiScopeWhenScopeValidationEnabled()
|
||||
{
|
||||
// Configure with scope validation enabled (default behavior)
|
||||
config.setIssuerUrl("https://login.serviceonline.alfresco/alfresco/v2.0");
|
||||
config.setScopeValidationDisabled(false);
|
||||
config.setPasswordGrantScopes("openid,profile,email," + CUSTOM_API_SCOPE);
|
||||
|
||||
try (MockedStatic<OIDCProviderMetadata> providerMetadata = Mockito.mockStatic(OIDCProviderMetadata.class))
|
||||
{
|
||||
// Simulate IDP: scopes_supported does NOT include custom API scope
|
||||
when(oidcResponse.getScopes()).thenReturn(new Scope("openid", "profile", "email", "offline_access"));
|
||||
providerMetadata.when(() -> OIDCProviderMetadata.parse(any(String.class))).thenReturn(oidcResponse);
|
||||
|
||||
ClientRegistration clientRegistration = new ClientRegistrationProvider(config).createClientRegistration(
|
||||
restTemplate);
|
||||
|
||||
// Custom API scope should be filtered out when scope validation is enabled
|
||||
assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("openid", "profile", "email");
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject createJSONObject(String fieldName, String fieldValue)
|
||||
{
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
+36
-8
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 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.authentication.admin;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -67,6 +68,8 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
||||
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
|
||||
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
|
||||
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
|
||||
private static final String REDIRECT_PATH = "/alfresco/s/admin/admin-communitysummary";
|
||||
private static final String CUSTOM_API_SCOPE = "api://client-id/alf_access";
|
||||
|
||||
@Mock
|
||||
HttpServletRequest request;
|
||||
@@ -155,13 +158,11 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
||||
@Test
|
||||
public void shouldCallAuthChallenge() throws IOException
|
||||
{
|
||||
String redirectPath = "/alfresco/s/admin/admin-communitysummary";
|
||||
|
||||
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
|
||||
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn("/alfresco/s/admin/admin-communitysummary");
|
||||
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
|
||||
.formatted("http://localhost:8080", redirectPath);
|
||||
.formatted("http://localhost:8080", REDIRECT_PATH);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
@@ -178,13 +179,12 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
||||
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);
|
||||
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
|
||||
.formatted("http://localhost:8080", redirectPath);
|
||||
.formatted("http://localhost:8080", REDIRECT_PATH);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
@@ -246,4 +246,32 @@ public class IdentityServiceAdminConsoleAuthenticatorUnitTest
|
||||
|
||||
assertEquals("admin", username);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnAllConfiguredScopesWhenScopeValidationIsDisabled() throws IOException
|
||||
{
|
||||
when(identityServiceConfig.isScopeValidationDisabled()).thenReturn(true);
|
||||
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "profile", CUSTOM_API_SCOPE));
|
||||
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
verify(response).sendRedirect(authenticationRequest.capture());
|
||||
assertThat(authenticationRequest.getValue()).contains("openid", "profile", CUSTOM_API_SCOPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFilterScopesAgainstProviderWhenValidationEnabled() throws IOException
|
||||
{
|
||||
when(identityServiceConfig.isScopeValidationDisabled()).thenReturn(false);
|
||||
when(identityServiceConfig.getAdminConsoleScopes()).thenReturn(Set.of("openid", "profile", CUSTOM_API_SCOPE));
|
||||
when(identityServiceConfig.getAdminConsoleRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
verify(response).sendRedirect(authenticationRequest.capture());
|
||||
assertThat(authenticationRequest.getValue()).contains("openid", "profile").doesNotContain(CUSTOM_API_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-8
@@ -2,7 +2,7 @@
|
||||
* #%L
|
||||
* Alfresco Repository
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2025 Alfresco Software Limited
|
||||
* Copyright (C) 2005 - 2026 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.webscript;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -69,6 +70,8 @@ public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest
|
||||
private static final String ALFRESCO_ACCESS_TOKEN = "ALFRESCO_ACCESS_TOKEN";
|
||||
private static final String ALFRESCO_REFRESH_TOKEN = "ALFRESCO_REFRESH_TOKEN";
|
||||
private static final String ALFRESCO_TOKEN_EXPIRATION = "ALFRESCO_TOKEN_EXPIRATION";
|
||||
private static final String REDIRECT_PATH = "/alfresco/s/index";
|
||||
private static final String CUSTOM_API_SCOPE = "api://client-id/alf_access";
|
||||
|
||||
@Mock
|
||||
HttpServletRequest request;
|
||||
@@ -157,14 +160,12 @@ public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest
|
||||
@Test
|
||||
public void shouldCallAuthChallengeWebScriptHome() throws IOException
|
||||
{
|
||||
|
||||
String redirectPath = "/alfresco/s/index";
|
||||
when(request.getRequestURL()).thenReturn(webScriptHomeURL);
|
||||
when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath);
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
|
||||
.formatted("http://localhost:8080", redirectPath);
|
||||
.formatted("http://localhost:8080", REDIRECT_PATH);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
@@ -181,14 +182,13 @@ public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest
|
||||
public void shouldCallAuthChallengeWebScriptHomeWithAudience() throws IOException
|
||||
{
|
||||
String audience = "http://localhost:8082";
|
||||
String redirectPath = "/alfresco/s/index";
|
||||
when(request.getRequestURL()).thenReturn(webScriptHomeURL);
|
||||
when(identityServiceConfig.getAudience()).thenReturn(audience);
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(redirectPath);
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "email", "profile", "offline_access"));
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
String expectedUri = "http://localhost:8999/auth?client_id=alfresco&redirect_uri=%s%s&response_type=code&scope="
|
||||
.formatted("http://localhost:8080", redirectPath);
|
||||
.formatted("http://localhost:8080", REDIRECT_PATH);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
@@ -250,4 +250,32 @@ public class IdentityServiceWebScriptsHomeAuthenticatorUnitTest
|
||||
|
||||
assertEquals("admin", username);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnAllConfiguredScopesWhenScopeValidationIsDisabled() throws IOException
|
||||
{
|
||||
when(identityServiceConfig.isScopeValidationDisabled()).thenReturn(true);
|
||||
when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "profile", CUSTOM_API_SCOPE));
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
verify(response).sendRedirect(authenticationRequest.capture());
|
||||
assertThat(authenticationRequest.getValue()).contains("openid", "profile", CUSTOM_API_SCOPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFilterScopesAgainstProviderWhenValidationEnabled() throws IOException
|
||||
{
|
||||
when(identityServiceConfig.isScopeValidationDisabled()).thenReturn(false);
|
||||
when(identityServiceConfig.getWebScriptsHomeScopes()).thenReturn(Set.of("openid", "profile", CUSTOM_API_SCOPE));
|
||||
when(identityServiceConfig.getWebScriptsHomeRedirectPath()).thenReturn(REDIRECT_PATH);
|
||||
ArgumentCaptor<String> authenticationRequest = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
authenticator.requestAuthentication(request, response);
|
||||
|
||||
verify(response).sendRedirect(authenticationRequest.capture());
|
||||
assertThat(authenticationRequest.getValue()).contains("openid", "profile").doesNotContain(CUSTOM_API_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user