From 6dc9d0d439dd60e1253b8a8faee2cb3fc9099f05 Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Tue, 26 Aug 2025 15:40:51 +0200 Subject: [PATCH] Check manage-account-links role for client initiated account linking Closes #41914 Signed-off-by: Giuseppe Graziano --- .../resources/IdentityBrokerService.java | 4 +- .../broker/KcOidcBrokerIdpLinkActionTest.java | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 0aa2b8ee81b..460a1ba4a1c 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -1001,7 +1001,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return redirectToErrorWhenLinkingFailed(authSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, idpDisplayName); } - if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.MANAGE_ACCOUNT))) { + RoleModel manageAccountRole = this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.MANAGE_ACCOUNT); + RoleModel manageAccountLinkRole = this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.MANAGE_ACCOUNT_LINKS); + if (!authenticatedUser.hasRole(manageAccountRole) && !authenticatedUser.hasRole(manageAccountLinkRole)) { return redirectToErrorPage(authSession, Response.Status.FORBIDDEN, Messages.INSUFFICIENT_PERMISSION); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerIdpLinkActionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerIdpLinkActionTest.java index 6d2a56eadc6..d411d1dfca2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerIdpLinkActionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerIdpLinkActionTest.java @@ -38,6 +38,7 @@ import org.keycloak.common.util.UriUtils; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.models.AccountRoles; import org.keycloak.models.Constants; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -45,6 +46,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.messages.Messages; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.IdpLinkActionPage; import org.keycloak.testsuite.util.AccountHelper; import org.keycloak.testsuite.util.oauth.OAuthClient; @@ -319,6 +321,84 @@ public class KcOidcBrokerIdpLinkActionTest extends AbstractInitializedBaseBroker } + @Test + public void testAccountLinkingWithDirectRoleManageAccount() throws Exception { + // Remove "manage-account" role from user + RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName()); + String user1Id = consumerRealm.users().search("user1").iterator().next().getId(); + + RoleRepresentation defaultRoles = consumerRealm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + bc.consumerRealmName()).toRepresentation(); + consumerRealm.users().get(user1Id).roles().realmLevel().remove(Collections.singletonList(defaultRoles)); + + ClientRepresentation accountClient = ApiUtil.findClientResourceByClientId(consumerRealm, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).toRepresentation(); + RoleRepresentation manageAccount = consumerRealm.clients().get(accountClient.getId()).roles().get(AccountRoles.MANAGE_ACCOUNT).toRepresentation(); + consumerRealm.users().get(user1Id).roles().clientLevel(accountClient.getId()).add(Collections.singletonList(manageAccount)); + + loginToConsumer(); + + // Redirect to link account on behalf of "broker-app" and login to the IDP + String kcAction = getKcActionParamForLinkIdp(bc.getIDPAlias()); + oauth.loginForm().kcAction(kcAction).open(); + confirmIdpLinking(); + + // Login to provider + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + events.clear(); + grantPage.assertCurrent(); + grantPage.accept(); + + appPage.assertCurrent(); + assertKcActionParams(IdpLinkAction.PROVIDER_ID, RequiredActionContext.KcActionStatus.SUCCESS.name().toLowerCase()); + + // Check that user is linked to the IDP + assertUserLinkedToIDP(true); + + assertEvents((providerRealmId, providerUserId, consumerRealmId, consumerUserId, consumerUsername) -> { + assertProviderEventsSuccess(providerRealmId, providerUserId); + assertConsumerSuccessLinkEvents(consumerRealmId, consumerUserId, consumerUsername); + }); + } + + @Test + public void testAccountLinkingWithDirectRoleManageAccountLinks() throws Exception { + // Remove "manage-account-link" role from user + RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName()); + String user1Id = consumerRealm.users().search("user1").iterator().next().getId(); + + RoleRepresentation defaultRoles = consumerRealm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + bc.consumerRealmName()).toRepresentation(); + consumerRealm.users().get(user1Id).roles().realmLevel().remove(Collections.singletonList(defaultRoles)); + + ClientRepresentation accountClient = ApiUtil.findClientResourceByClientId(consumerRealm, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).toRepresentation(); + RoleRepresentation manageAccountLinks = consumerRealm.clients().get(accountClient.getId()).roles().get(AccountRoles.MANAGE_ACCOUNT_LINKS).toRepresentation(); + consumerRealm.users().get(user1Id).roles().clientLevel(accountClient.getId()).add(Collections.singletonList(manageAccountLinks)); + + loginToConsumer(); + + // Redirect to link account on behalf of "broker-app" and login to the IDP + String kcAction = getKcActionParamForLinkIdp(bc.getIDPAlias()); + oauth.loginForm().kcAction(kcAction).open(); + confirmIdpLinking(); + + // Login to provider + loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + + events.clear(); + grantPage.assertCurrent(); + grantPage.accept(); + + appPage.assertCurrent(); + assertKcActionParams(IdpLinkAction.PROVIDER_ID, RequiredActionContext.KcActionStatus.SUCCESS.name().toLowerCase()); + + // Check that user is linked to the IDP + assertUserLinkedToIDP(true); + + assertEvents((providerRealmId, providerUserId, consumerRealmId, consumerUserId, consumerUsername) -> { + assertProviderEventsSuccess(providerRealmId, providerUserId); + assertConsumerSuccessLinkEvents(consumerRealmId, consumerUserId, consumerUsername); + }); + } + @Test public void testConsumerReauthentication() throws Exception { loginToConsumer();