User Profile: If required roles ('user') and reqired scopes are set, the required scopes have no effect

closes #25475

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2023-12-13 10:30:59 +01:00 committed by Marek Posolda
parent 59536becec
commit cd154cf318
3 changed files with 88 additions and 1 deletions

View file

@ -296,6 +296,17 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
if (rc != null) { if (rc != null) {
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) { if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
required = AttributeMetadata.ALWAYS_TRUE; required = AttributeMetadata.ALWAYS_TRUE;
// If scopes are configured, we will use scope-based selector and require the attribute just if scope is
// in current authenticationSession (either default scope or by 'scope' parameter)
if (rc.getScopes() != null && !rc.getScopes().isEmpty()) {
if (UPConfigUtils.canBeAuthFlowContext(context)) {
required = (c) -> requestedScopePredicate(c, rc.getScopes());
} else {
// Scopes not available for admin and account contexts
required = AttributeMetadata.ALWAYS_FALSE;
}
}
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) { } else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
// for contexts executed from auth flow and with configured scopes requirement // for contexts executed from auth flow and with configured scopes requirement
// we have to create required validation with scopes based selector // we have to create required validation with scopes based selector

View file

@ -29,6 +29,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.junit.Before; import org.junit.Before;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -214,7 +215,11 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
@Override @Override
public String getClientNote(String name) { public String getClientNote(String name) {
return null; if (OAuth2Constants.SCOPE.equals(name) && scopes != null && !scopes.isEmpty()) {
return String.join(" ", scopes);
} else {
return null;
}
} }
@Override @Override

View file

@ -61,6 +61,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
import org.keycloak.representations.userprofile.config.UPGroup; import org.keycloak.representations.userprofile.config.UPGroup;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.userprofile.AttributeGroupMetadata; import org.keycloak.userprofile.AttributeGroupMetadata;
@ -98,8 +99,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
testRealm.setClientScopes(new ArrayList<>()); testRealm.setClientScopes(new ArrayList<>());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build()); testRealm.getClientScopes().add(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build()); testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("some-optional-scope").protocol("openid-connect").build());
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a"); ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
client.setDefaultClientScopes(Collections.singletonList("customer")); client.setDefaultClientScopes(Collections.singletonList("customer"));
client.setOptionalClientScopes(Collections.singletonList("some-optional-scope"));
KeycloakModelUtils.createClient(testRealm, "client-b"); KeycloakModelUtils.createClient(testRealm, "client-b");
} }
@ -1429,6 +1432,74 @@ public class UserProfileTest extends AbstractUserProfileTest {
} }
@Test
@ModelTest
public void testRequiredByOptionalClientScope(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("test");
session.getContext().setRealm(realm);
UserProfileProvider provider = getUserProfileProvider(session);
UPConfig config = parseDefaultConfig();
config.addOrReplaceAttribute(new UPAttribute(ATT_ADDRESS, new UPAttributePermissions(Set.of(), Set.of(ROLE_ADMIN, ROLE_USER)), new UPAttributeRequired(Set.of(ROLE_ADMIN, ROLE_USER), Set.of("some-optional-scope"))));
provider.setConfiguration(config);
Map<String, Object> attributes = new HashMap<>();
attributes.put(UserModel.USERNAME, "user");
attributes.put(UserModel.FIRST_NAME, "John");
attributes.put(UserModel.LAST_NAME, "Doe");
attributes.put(UserModel.EMAIL, "user@email.test");
// client with default scopes. No address scope included
configureAuthenticationSession(session, "client-a", null);
// No fail on admin and account console as they do not have scopes
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
profile.validate();
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
profile.validate();
// no fail on auth flow scopes when scope is not required
profile = provider.create(UserProfileContext.REGISTRATION, attributes);
profile.validate();
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
profile.validate();
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
profile.validate();
// client with default scopes for which is attribute NOT configured as required
configureAuthenticationSession(session, "client-a", Set.of("some-optional-scope"));
// No fail on admin and account console as they do not have scopes
profile = provider.create(UserProfileContext.USER_API, attributes);
profile.validate();
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
profile.validate();
// fail on auth flow scopes when scope is required
try {
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
profile.validate();
fail("Should fail validation");
} catch (ValidationException ve) {
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
}
try {
profile = provider.create(UserProfileContext.REGISTRATION, attributes);
profile.validate();
fail("Should fail validation");
} catch (ValidationException ve) {
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
}
try {
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
profile.validate();
fail("Should fail validation");
} catch (ValidationException ve) {
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
}
}
@Test @Test
public void testConfigurationInvalidScope() { public void testConfigurationInvalidScope() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationInvalidScope); getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationInvalidScope);