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:
parent
aacdf80664
commit
35eba8be8c
6 changed files with 68 additions and 6 deletions
|
@ -93,6 +93,7 @@ public interface OAuth2Constants {
|
||||||
String SCOPE_PHONE = "phone";
|
String SCOPE_PHONE = "phone";
|
||||||
|
|
||||||
String ORGANIZATION = "organization";
|
String ORGANIZATION = "organization";
|
||||||
|
String ORGANIZATION_ID = "id";
|
||||||
|
|
||||||
String UI_LOCALES_PARAM = "ui_locales";
|
String UI_LOCALES_PARAM = "ui_locales";
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
|
@ -11,6 +11,7 @@ As a result, the token will contain a claim as follows:
|
||||||
```json
|
```json
|
||||||
"organization": {
|
"organization": {
|
||||||
"testcorp": {
|
"testcorp": {
|
||||||
|
"id": "42c3e46f-2477-44d7-a85b-d3b43f6b31fa",
|
||||||
"attr1": [
|
"attr1": [
|
||||||
"value1"
|
"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.
|
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
|
.Including attributes in the organization claim
|
||||||
image:images/organizations-add-org-attrs-in-claim.png[alt="Including attributes in the organization claim"]
|
image:images/organizations-add-org-attrs-in-claim.png[alt="Including attributes in the organization claim"]
|
||||||
|
|
|
@ -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.
|
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.label=Add organization attributes
|
||||||
addOrganizationAttributes.help=If enabled, the organization attributes will be available for each organization mapped to the token.
|
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?
|
identityProviderUnlink=Unlink identity provider?
|
||||||
identityProviderUnlinkConfirm=Are you sure you want to unlink this identity provider?
|
identityProviderUnlinkConfirm=Are you sure you want to unlink this identity provider?
|
||||||
disableConfirmUserTitle=Disable user?
|
disableConfirmUserTitle=Disable user?
|
||||||
|
|
|
@ -56,6 +56,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "oidc-organization-membership-mapper";
|
public static final String PROVIDER_ID = "oidc-organization-membership-mapper";
|
||||||
public static final String ADD_ORGANIZATION_ATTRIBUTES = "addOrganizationAttributes";
|
public static final String ADD_ORGANIZATION_ATTRIBUTES = "addOrganizationAttributes";
|
||||||
|
public static final String ADD_ORGANIZATION_ID = "addOrganizationId";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ProviderConfigProperty> getConfigProperties() {
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
@ -77,6 +78,13 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
property.setDefaultValue(Boolean.FALSE.toString());
|
property.setDefaultValue(Boolean.FALSE.toString());
|
||||||
property.setHelpText(ADD_ORGANIZATION_ATTRIBUTES + ".help");
|
property.setHelpText(ADD_ORGANIZATION_ATTRIBUTES + ".help");
|
||||||
properties.add(property);
|
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;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +145,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!OIDCAttributeMapperHelper.isMultivalued(model)) {
|
if (!OIDCAttributeMapperHelper.isMultivalued(model)) {
|
||||||
return organizations.get(0).getName();
|
return organizations.get(0).getAlias();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Map<String, Object>> value = new HashMap<>();
|
Map<String, Map<String, Object>> value = new HashMap<>();
|
||||||
|
@ -147,13 +155,16 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
continue;
|
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)) {
|
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()) {
|
if (value.isEmpty()) {
|
||||||
|
@ -181,9 +192,10 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
|
|
||||||
if (!OIDCAttributeMapperHelper.isMultivalued(copy)) {
|
if (!OIDCAttributeMapperHelper.isMultivalued(copy)) {
|
||||||
config.put(ADD_ORGANIZATION_ATTRIBUTES, Boolean.FALSE.toString());
|
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");
|
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()));
|
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) {
|
public static ProtocolMapperModel create(String name, boolean accessToken, boolean idToken, boolean introspectionEndpoint) {
|
||||||
ProtocolMapperModel mapper = new ProtocolMapperModel();
|
ProtocolMapperModel mapper = new ProtocolMapperModel();
|
||||||
mapper.setName(name);
|
mapper.setName(name);
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.testsuite.organization.mapper;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
|
@ -452,6 +453,47 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
||||||
assertThat(organizations.get(organizationName).keySet().isEmpty(), is(true));
|
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
|
@Test
|
||||||
public void testOrganizationsClaimAsList() throws Exception {
|
public void testOrganizationsClaimAsList() throws Exception {
|
||||||
OrganizationRepresentation orgA = createOrganization("orga", true);
|
OrganizationRepresentation orgA = createOrganization("orga", true);
|
||||||
|
|
Loading…
Reference in a new issue