The existence of an organization attribute called id is not validated

Closes #44522

Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
Martin Kanis 2025-12-10 10:39:02 +01:00 committed by Pedro Igor
parent bf18942c34
commit 012cefb654
3 changed files with 49 additions and 4 deletions

View file

@ -171,6 +171,14 @@ See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to
For each expired user session there is a new user event `USER_SESSION_DELETED` fired.
As part of this change, the process now deletes rows from the table in small batches, instead of issuing a delete statements that affects the whole table. This should allow for better response times when there are a lot of sessions in the table.
=== Organization custom attribute named 'id' behavior change
Organizations can have custom attributes named `id`. When both organization attributes and organization ID are included in tokens via the organization membership mapper configuration,
the organization ID will override any custom `id` attribute value. Previously, the organization ID was added first and could be overridden by custom attributes.
This ensures that the `id` field in the organization claims always contains the actual organization ID.
If you have a custom organization attribute named `id` and rely on its value in tokens, you should rename the attribute to avoid it being overridden by the organization ID.
// ------------------------ Deprecated features ------------------------ //
== Deprecated features

View file

@ -162,12 +162,14 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
Map<String, Object> claims = new HashMap<>();
if (isAddOrganizationId(model)) {
claims.put(OAuth2Constants.ORGANIZATION_ID, o.getId());
}
// Add organization attributes first
if (isAddOrganizationAttributes(model)) {
claims.putAll(o.getAttributes());
}
// Add organization ID last so it overrides any custom "id" attribute
if (isAddOrganizationId(model)) {
claims.put(OAuth2Constants.ORGANIZATION_ID, o.getId());
}
value.put(o.getAlias(), claims);
}

View file

@ -79,8 +79,8 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
@Before
public void onBefore() {
setMapperConfig(OIDCAttributeMapperHelper.JSON_TYPE, null);
setMapperConfig(ProtocolMapperUtils.MULTIVALUED, null);
setMapperConfig(OIDCAttributeMapperHelper.JSON_TYPE, null);
}
@Test
@ -984,6 +984,41 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
assertThat(organization, containsInAnyOrder("orga", "orgb"));
}
@Test
@SuppressWarnings("unchecked")
public void testOrganizationAttributeNamedIdIsOverriddenByOrganizationId() throws Exception {
// When an organization has a custom attribute called "id", the organization ID should override it in tokens
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
addMember(organization);
// Add a custom attribute named "id" to the organization
orgRep.singleAttribute("id", "custom-id-value");
try (Response response = organization.update(orgRep)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
// Verify that organization ID overrides custom "id" attribute in tokens
setMapperConfig(OrganizationMembershipMapper.ADD_ORGANIZATION_ID, Boolean.TRUE.toString());
setMapperConfig(OrganizationMembershipMapper.ADD_ORGANIZATION_ATTRIBUTES, Boolean.TRUE.toString());
oauth.client("direct-grant", "password");
oauth.scope("openid organization");
AccessTokenResponse response = oauth.doPasswordGrantRequest(memberEmail, memberPassword);
assertThat(response.getScope(), containsString("organization"));
AccessToken accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken();
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
Map<String, Map<String, String>> organizations = (Map<String, Map<String, String>>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
assertThat(organizations.keySet(), hasItem(organizationName));
Map<String, String> orgClaims = organizations.get(organizationName);
// The "id" attribute should contain the organization ID, not the custom value
assertThat(orgClaims.get("id"), equalTo(orgRep.getId()));
assertThat(orgClaims.get("id"), not(equalTo("custom-id-value")));
}
private AccessTokenResponse assertSuccessfulCodeGrant() {
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse response = oauth.doAccessTokenRequest(code);