Add option to include the organization id in the organization claims

Closes #32746

Signed-off-by: Maksim Zvankovich <m.zvankovich@nexovagroup.eu>
Co-authored-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Maksim Zvankovich 2024-09-11 14:16:41 +02:00 committed by Pedro Igor
parent aacdf80664
commit 35eba8be8c
6 changed files with 68 additions and 6 deletions

View file

@ -93,6 +93,7 @@ public interface OAuth2Constants {
String SCOPE_PHONE = "phone";
String ORGANIZATION = "organization";
String ORGANIZATION_ID = "id";
String UI_LOCALES_PARAM = "ui_locales";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View file

@ -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"]

View file

@ -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?

View file

@ -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<ProviderConfigProperty> 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<String, Map<String, Object>> value = new HashMap<>();
@ -147,13 +155,16 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
continue;
}
Map<String, Object> attributes = Map.of();
Map<String, Object> 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);

View file

@ -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<String, Map<String, String>> organizations = (Map<String, Map<String, String>>) 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<String, Map<String, String>>) 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<String, Map<String, String>>) 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);