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.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
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()) {
// for contexts executed from auth flow and with configured scopes requirement
// 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 org.junit.Before;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
@ -214,8 +215,12 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
@Override
public String getClientNote(String name) {
if (OAuth2Constants.SCOPE.equals(name) && scopes != null && !scopes.isEmpty()) {
return String.join(" ", scopes);
} else {
return null;
}
}
@Override
public void setClientNote(String name, String value) {

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.UPGroup;
import org.keycloak.services.messages.Messages;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.userprofile.AttributeGroupMetadata;
@ -98,8 +99,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
testRealm.setClientScopes(new ArrayList<>());
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("some-optional-scope").protocol("openid-connect").build());
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
client.setDefaultClientScopes(Collections.singletonList("customer"));
client.setOptionalClientScopes(Collections.singletonList("some-optional-scope"));
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
public void testConfigurationInvalidScope() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationInvalidScope);