From 2dab08d5ed08825628ea54364e97ba38e806d574 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 30 Jan 2026 17:17:35 -0300 Subject: [PATCH] Make sure disabled organizations are not available from selection Closes #45874 Signed-off-by: Pedro Igor --- .../browser/OrganizationAuthenticator.java | 24 ++-- .../mappers/oidc/OrganizationScope.java | 2 +- .../OrganizationOIDCProtocolMapperTest.java | 104 +++++++++++++++++- 3 files changed, 115 insertions(+), 15 deletions(-) diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java index 473b24dd3e0..12f1b7eb4b9 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -199,27 +199,27 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { MultivaluedMap parameters = request.getDecodedFormParameters(); // parameter from the organization selection page List alias = parameters.getOrDefault(OrganizationModel.ORGANIZATION_ATTRIBUTE, List.of()); + OrganizationModel organization; if (alias.isEmpty()) { - OrganizationModel organization = Organizations.resolveOrganization(session, user, domain); - - if (isSSOAuthentication(authSession) && organization != null) { + organization = Organizations.resolveOrganization(session, user, domain); + if (organization != null && isSSOAuthentication(authSession)) { // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); } - - return organization; + } else { + OrganizationProvider provider = getOrganizationProvider(); + organization = provider.getByAlias(alias.get(0)); } - OrganizationProvider provider = getOrganizationProvider(); - OrganizationModel organization = provider.getByAlias(alias.get(0)); - - if (organization == null) { + if (organization == null || !organization.isEnabled()) { return null; } - // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens - authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); + if (!alias.isEmpty() || isSSOAuthentication(authSession)) { + // make sure the organization selected by the user is available from the client session when running mappers and issuing tokens + authSession.setClientNote(OrganizationModel.ORGANIZATION_ATTRIBUTE, organization.getId()); + } return organization; } @@ -237,7 +237,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { } OrganizationProvider provider = getOrganizationProvider(); - Stream organizations = provider.getByMember(user); + Stream organizations = provider.getByMember(user).filter(OrganizationModel::isEnabled); if (organizations.count() > 1) { LoginFormsProvider form = context.form(); diff --git a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationScope.java b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationScope.java index aa31e49d816..5ff73ac059a 100644 --- a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationScope.java +++ b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationScope.java @@ -116,7 +116,7 @@ public enum OrganizationScope { return Stream.empty(); } - List organizations = getProvider(session).getByMember(user).toList(); + List organizations = getProvider(session).getByMember(user).filter(OrganizationModel::isEnabled).toList(); if (organizations.size() == 1) { return organizations.stream(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java index c939d990201..54c4da5c312 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/mapper/OrganizationOIDCProtocolMapperTest.java @@ -272,14 +272,114 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest selectOrganizationPage.selectOrganization(orgB.getAlias()); loginPage.login(memberPassword); AccessTokenResponse response = assertSuccessfulCodeGrant(); - // for now, return the organization scope in the response and access token even though no organization is mapped into the token - // once we support the user to select an organization, the selected organization will be mapped assertThat(response.getScope(), containsString("organization")); AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); assertThat(organizations.contains(orgA.getAlias()), is(false)); assertThat(organizations.contains(orgB.getAlias()), is(true)); + + testRealm().users().get(member.getId()).logout(); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + selectOrganizationPage.assertCurrent(); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgA.getAlias())); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); + orgB.setEnabled(false); + testRealm().organizations().get(orgB.getId()).update(orgB).close(); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + assertFalse(selectOrganizationPage.isCurrent()); + loginPage.login(memberPassword); + response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString("organization")); + accessToken = oauth.verifyToken(response.getAccessToken()); + organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + assertThat(organizations.contains(orgA.getAlias()), is(true)); + assertThat(organizations.contains(orgB.getAlias()), is(false)); + } + + @Test + public void testOrganizationScopeSelectDisabledOrganization() { + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + OrganizationRepresentation orgB = createOrganization("orgb", true); + testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); + oauth.client("broker-app", KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + oauth.scope("organization"); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + assertTrue(selectOrganizationPage.isCurrent()); + assertFalse(driver.getPageSource().contains("kc-select-try-another-way-form")); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgA.getAlias())); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); + selectOrganizationPage.selectOrganization(orgB.getAlias()); + loginPage.login(memberPassword); + AccessTokenResponse response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString("organization")); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + assertThat(organizations.contains(orgA.getAlias()), is(false)); + assertThat(organizations.contains(orgB.getAlias()), is(true)); + + testRealm().users().get(member.getId()).logout(); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + selectOrganizationPage.assertCurrent(); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgA.getAlias())); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); + orgB.setEnabled(false); + testRealm().organizations().get(orgB.getId()).update(orgB).close(); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + assertFalse(selectOrganizationPage.isCurrent()); + loginPage.login(memberPassword); + response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString("organization")); + accessToken = oauth.verifyToken(response.getAccessToken()); + organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + assertThat(organizations.contains(orgA.getAlias()), is(true)); + assertThat(organizations.contains(orgB.getAlias()), is(false)); + } + + @Test + public void testOrganizationScopeSpecifyDisabledOrganization() { + OrganizationRepresentation orgA = createOrganization("orga", true); + MemberRepresentation member = addMember(testRealm().organizations().get(orgA.getId()), "member@" + orgA.getDomains().iterator().next().getName()); + OrganizationRepresentation orgB = createOrganization("orgb", true); + testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close(); + oauth.client("broker-app", KcOidcBrokerConfiguration.CONSUMER_BROKER_APP_SECRET); + oauth.scope("organization"); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + assertTrue(selectOrganizationPage.isCurrent()); + assertFalse(driver.getPageSource().contains("kc-select-try-another-way-form")); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgA.getAlias())); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); + selectOrganizationPage.selectOrganization(orgB.getAlias()); + loginPage.login(memberPassword); + AccessTokenResponse response = assertSuccessfulCodeGrant(); + assertThat(response.getScope(), containsString("organization")); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + List organizations = (List) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + assertThat(organizations.contains(orgA.getAlias()), is(false)); + assertThat(organizations.contains(orgB.getAlias()), is(true)); + + testRealm().users().get(member.getId()).logout(); + loginPage.open(bc.consumerRealmName()); + loginPage.loginUsername(member.getEmail()); + selectOrganizationPage.assertCurrent(); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgA.getAlias())); + assertTrue(selectOrganizationPage.isOrganizationButtonPresent(orgB.getAlias())); + orgB.setEnabled(false); + testRealm().organizations().get(orgB.getId()).update(orgB).close(); + oauth.scope("organization:" + orgB.getAlias()); + oauth.openLoginForm(); + assertTrue(driver.getCurrentUrl().contains("Invalid+scopes%3A+openid+organization")); } @Test