Fix requesting tokens with multiple organization scopes

It was not possible to request tokens with more than one organization
scope. Modified TokenManager to collect all organization scopes into
a Set instead of keeping only the last one.

Closes #45900

Signed-off-by: maciej.kruszewski <maciej.kruszewski@symmetrical.ai>
This commit is contained in:
maciej.kruszewski 2026-01-30 14:18:39 +01:00
parent 13cf35ded3
commit 7f7599c1e3
3 changed files with 34 additions and 16 deletions

View file

@ -75,22 +75,14 @@ public enum OrganizationScope {
*/
SINGLE(StringUtil::isNotBlank,
(user, scopes, session) -> {
OrganizationModel organization = parseScopeParameter(session, scopes)
List<OrganizationModel> organizations = parseScopeParameter(session, scopes)
.map((String scope) -> parseScopeValue(session, scope))
.map(alias -> getProvider(session).getByAlias(alias))
.filter(Objects::nonNull)
.findAny()
.orElse(null);
.filter(org -> user == null || org.isMember(user))
.toList();
if (organization == null) {
return Stream.empty();
}
if (user == null || organization.isMember(user)) {
return Stream.of(organization);
}
return Stream.empty();
return organizations.stream();
},
(organizations) -> organizations.findAny().isPresent(),
(session, current, previous) -> {

View file

@ -723,10 +723,21 @@ public class TokenManager {
Collection<String> rawScopes = TokenManager.parseScopeParameter(scopes).collect(Collectors.toSet());
// detect multiple organization scopes
// validate organization scopes - allow multiple specific organization scopes, but reject mixed types
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
if (rawScopes.stream().filter(scope -> scope.startsWith(ORGANIZATION)).count() > 1) {
return false;
List<String> orgScopes = rawScopes.stream()
.filter(scope -> scope.equals(ORGANIZATION) || scope.startsWith(ORGANIZATION + ":"))
.toList();
if (orgScopes.size() > 1) {
// multiple organization scopes - only allow if all are specific organizations (not ANY or ALL)
boolean hasAnyScope = orgScopes.stream().anyMatch(ORGANIZATION::equals);
boolean hasAllScope = orgScopes.stream().anyMatch(s -> s.equals(ORGANIZATION + ":*"));
if (hasAnyScope || hasAllScope) {
// mixing ANY (organization) or ALL (organization:*) with other organization scopes is not allowed
return false;
}
}
}

View file

@ -115,6 +115,7 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
}
@Test
@SuppressWarnings("unchecked")
public void testMultipleOrganizationScopes() throws Exception {
OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
@ -129,14 +130,28 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
Assert.assertTrue(orgb.members().list(-1, -1).stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
oauth.clientId("test-app");
oauth.scope("openid organization organization:org-a");
// Test multiple specific organization scopes - should return both organizations
oauth.scope("openid organization:org-a organization:org-b");
AccessTokenResponse response = oauth.doPasswordGrantRequest(memberEmail, memberPassword);
Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken();
List<String> organizations = (List<String>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
Assert.assertNotNull(organizations);
Assert.assertTrue(organizations.contains("org-a"));
Assert.assertTrue(organizations.contains("org-b"));
// Test organization + specific organization scope - should still fail (mixing ANY with SINGLE)
oauth.scope("openid organization organization:org-a");
response = oauth.doPasswordGrantRequest(memberEmail, memberPassword);
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
// Test organization + wildcard scope - should still fail (mixing ANY with ALL)
oauth.scope("openid organization organization:*");
response = oauth.doPasswordGrantRequest(memberEmail, memberPassword);
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
// Test specific organization + wildcard scope - should still fail (mixing SINGLE with ALL)
oauth.scope("openid organization:org-a organization:*");
response = oauth.doPasswordGrantRequest(memberEmail, memberPassword);
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());