diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index b03dd25554..389aab1aae 100755 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -93,6 +93,7 @@ public interface OAuth2Constants { String SCOPE_PHONE = "phone"; String ORGANIZATION = "organization"; + String ORGANIZATION_ID = "id"; String UI_LOCALES_PARAM = "ui_locales"; diff --git a/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png b/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png index 5a4afd6afa..c1184f9d5a 100644 Binary files a/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png and b/docs/documentation/server_admin/images/organizations-add-org-attrs-in-claim.png differ diff --git a/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc b/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc index 819c20e878..4dec2ba77c 100644 --- a/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc +++ b/docs/documentation/server_admin/topics/organizations/mapping-organization-claims.adoc @@ -11,6 +11,7 @@ As a result, the token will contain a claim as follows: ```json "organization": { "testcorp": { + "id": "42c3e46f-2477-44d7-a85b-d3b43f6b31fa", "attr1": [ "value1" ] @@ -23,7 +24,7 @@ to authorize access to protected resources based on the organization where the u The `organization` scope is a built-in optional client scope at the realm. Therefore, this scope is added to any client created in the realm by default. It also defines the `Organization Membership` mapper that controls how the organization membership information is mapped to the tokens. -By default, the organization attributes are not included in the organization claim. To include the attributes in the claim, edit the mapper and enable *Add organization attributes*. +NOTE: By default, the organization id and attributes are not included in the organization claim. To include them, edit the mapper and enable the *Add organization id* and *Add organization attributes* options, respectively. .Including attributes in the organization claim image:images/organizations-add-org-attrs-in-claim.png[alt="Including attributes in the organization claim"] diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 21394b6214..aad98e85ab 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3246,6 +3246,8 @@ temporaryAdmin=Temporary admin user account. Ensure it is replaced with a perman temporaryService=Temporary admin service account. Ensure it is replaced with a permanent admin service account as soon as possible. addOrganizationAttributes.label=Add organization attributes addOrganizationAttributes.help=If enabled, the organization attributes will be available for each organization mapped to the token. +addOrganizationId.label=Add organization id +addOrganizationId.help=If enabled, the organization id will be available for each organization mapped to the token. identityProviderUnlink=Unlink identity provider? identityProviderUnlinkConfirm=Are you sure you want to unlink this identity provider? disableConfirmUserTitle=Disable user? diff --git a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java index 5d2654daad..2c33989911 100644 --- a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java +++ b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java @@ -56,6 +56,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp public static final String PROVIDER_ID = "oidc-organization-membership-mapper"; public static final String ADD_ORGANIZATION_ATTRIBUTES = "addOrganizationAttributes"; + public static final String ADD_ORGANIZATION_ID = "addOrganizationId"; @Override public List getConfigProperties() { @@ -77,6 +78,13 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp property.setDefaultValue(Boolean.FALSE.toString()); property.setHelpText(ADD_ORGANIZATION_ATTRIBUTES + ".help"); properties.add(property); + property = new ProviderConfigProperty(); + property.setName(ADD_ORGANIZATION_ID); + property.setLabel(ADD_ORGANIZATION_ID + ".label"); + property.setType(ProviderConfigProperty.BOOLEAN_TYPE); + property.setDefaultValue(Boolean.FALSE.toString()); + property.setHelpText(ADD_ORGANIZATION_ID + ".help"); + properties.add(property); return properties; } @@ -137,7 +145,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp } if (!OIDCAttributeMapperHelper.isMultivalued(model)) { - return organizations.get(0).getName(); + return organizations.get(0).getAlias(); } Map> value = new HashMap<>(); @@ -147,13 +155,16 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp continue; } - Map attributes = Map.of(); + Map claims = new HashMap<>(); + if (isAddOrganizationId(model)) { + claims.put(OAuth2Constants.ORGANIZATION_ID, o.getId()); + } if (isAddOrganizationAttributes(model)) { - attributes = new HashMap<>(o.getAttributes()); + claims.putAll(o.getAttributes()); } - value.put(o.getAlias(), attributes); + value.put(o.getAlias(), claims); } if (value.isEmpty()) { @@ -181,9 +192,10 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp if (!OIDCAttributeMapperHelper.isMultivalued(copy)) { config.put(ADD_ORGANIZATION_ATTRIBUTES, Boolean.FALSE.toString()); + config.put(ADD_ORGANIZATION_ID, Boolean.FALSE.toString()); } - if (isAddOrganizationAttributes(copy)) { + if (isAddOrganizationAttributes(copy) || isAddOrganizationId(copy)) { config.put(JSON_TYPE, "JSON"); } @@ -208,6 +220,10 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp return Boolean.parseBoolean(model.getConfig().getOrDefault(ADD_ORGANIZATION_ATTRIBUTES, Boolean.FALSE.toString())); } + private boolean isAddOrganizationId(ProtocolMapperModel model) { + return Boolean.parseBoolean(model.getConfig().getOrDefault(ADD_ORGANIZATION_ID, Boolean.FALSE.toString())); + } + public static ProtocolMapperModel create(String name, boolean accessToken, boolean idToken, boolean introspectionEndpoint) { ProtocolMapperModel mapper = new ProtocolMapperModel(); mapper.setName(name); 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 31f79df841..1b44c3dc2b 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 @@ -20,6 +20,7 @@ package org.keycloak.testsuite.organization.mapper; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -452,6 +453,47 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest assertThat(organizations.get(organizationName).keySet().isEmpty(), is(true)); } + @Test + @SuppressWarnings("unchecked") + public void testIncludeOrganizationId() throws Exception { + OrganizationRepresentation orgRep = createOrganization(); + OrganizationResource organization = testRealm().organizations().get(orgRep.getId()); + addMember(organization); + setMapperConfig(OrganizationMembershipMapper.ADD_ORGANIZATION_ID, Boolean.TRUE.toString()); + + oauth.clientId("direct-grant"); + oauth.scope("openid organization"); + AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword); + assertThat(response.getScope(), containsString("organization")); + AccessToken accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken(); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + Map> organizations = (Map>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.keySet(), hasItem(organizationName)); + assertThat(organizations.get(organizationName).keySet(), hasItem("id")); + assertThat(organizations.get(organizationName).get("id"), equalTo(orgRep.getId())); + + // when id is added to tokens, the claim type is a json regardless of the value set in the config + setMapperConfig(OrganizationMembershipMapper.ADD_ORGANIZATION_ID, Boolean.TRUE.toString()); + setMapperConfig(OIDCAttributeMapperHelper.JSON_TYPE, "boolean"); + response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword); + accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken(); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + organizations = (Map>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.keySet(), hasItem(organizationName)); + assertThat(organizations.get(organizationName).keySet(), hasItem("id")); + assertThat(organizations.get(organizationName).get("id"), equalTo(orgRep.getId())); + + // disabling the attribute should result in no ids in the claims. + setMapperConfig(OrganizationMembershipMapper.ADD_ORGANIZATION_ID, Boolean.FALSE.toString()); + setMapperConfig(OIDCAttributeMapperHelper.JSON_TYPE, "JSON"); + response = oauth.doGrantAccessTokenRequest("password", memberEmail, memberPassword); + accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken(); + assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); + organizations = (Map>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); + assertThat(organizations.keySet(), hasItem(organizationName)); + assertThat(organizations.get(organizationName).keySet().isEmpty(), is(true)); + } + @Test public void testOrganizationsClaimAsList() throws Exception { OrganizationRepresentation orgA = createOrganization("orga", true);