Skip to content

Commit

Permalink
New token claim for a specific organization attribute (#234)
Browse files Browse the repository at this point in the history
* New token claim for a specific organization attribute

* Test for organization specific attribute claim mapper

* Filter null values., Testcase update
  • Loading branch information
prifulnath committed Jun 4, 2024
1 parent 7c48b7c commit eb1ac15
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.phasetwo.service.protocol.oidc.mappers;

import com.google.auto.service.AutoService;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.phasetwo.service.model.OrganizationProvider;
import java.util.List;
import java.util.Map;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;

@JBossLog
@AutoService(ProtocolMapper.class)
public class OrganizationSpecificAttributeMapper extends AbstractOrganizationMapper {

public static final String PROVIDER_ID = "oidc-organization-specific-attribute-mapper";

private static final List<ProviderConfigProperty> configProperties = Lists.newArrayList();

static {
OIDCAttributeMapperHelper.addAttributeConfig(
configProperties, OrganizationSpecificAttributeMapper.class);
}

public OrganizationSpecificAttributeMapper() {
super(
PROVIDER_ID,
"Organization Specific Attribute",
TOKEN_MAPPER_CATEGORY,
"Map organization single specific attributes in a token claim.",
configProperties);
}

@Override
protected Map<String, Object> getOrganizationClaim(
KeycloakSession session, RealmModel realm, UserModel user, ProtocolMapperModel mappingModel) {
OrganizationProvider orgs = session.getProvider(OrganizationProvider.class);
Map<String, Object> organizationClaim = Maps.newHashMap();
orgs.getUserOrganizationsStream(realm, user)
.forEach(
o -> {
// add to token only when value is available
String attributeValue = o.getFirstAttribute(mappingModel.getName());
if (attributeValue != null) {
organizationClaim.put(o.getId(), attributeValue);
}
});
log.debugf("created user %s organization claim %s", user.getUsername(), organizationClaim);
return organizationClaim;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package io.phasetwo.service.mapper;

import static io.phasetwo.service.Helpers.createUserWithCredentials;
import static io.phasetwo.service.Helpers.deleteUser;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import io.phasetwo.client.openapi.model.OrganizationRepresentation;
import io.phasetwo.service.AbstractOrganizationTest;
import io.phasetwo.service.protocol.oidc.mappers.OrganizationSpecificAttributeMapper;
import io.restassured.response.Response;
import jakarta.ws.rs.core.Response.Status;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.jbosslog.JBossLog;
import org.junit.jupiter.api.Test;
import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

@JBossLog
class OrganizationSpecificAttributeMapperTest extends AbstractOrganizationTest {

public static final String CLAIM = "secret_attr";
public static final String SECOND_CLAIM = "another_attribute";

@Test
void shouldConfigureOrganizationSpecificAttributeMapperOidcProtocolMapper() throws Exception {
// add Example 1 with attribute 'secret_attr' with value "My Secret Value"
var organization1 = createOrganization(
new OrganizationRepresentation()
.name("example1")
.domains(List.of("example1.com"))
.url("www.example1.com")
.displayName("Example 1")
.attributes(Map.of(CLAIM, List.of("My Secret Value"))));
String organizationId1 = organization1.getId();

// add Example 2 with attribute 'secret_attr', value "My Second Secret Value"
var organization2 = createOrganization(
new OrganizationRepresentation()
.name("example2")
.domains(List.of("example2.com"))
.url("www.example2.com")
.displayName("Example 2")
.attributes(Map.of(CLAIM, List.of("My Second Secret Value"))));
String organizationId2 = organization2.getId();

// add organization Example 3 with attribute 'secret_attr' with no value
var organization3 = createOrganization(
new OrganizationRepresentation()
.name("example3")
.domains(List.of("example3.com"))
.url("www.example3.com")
.displayName("Example 3")
.attributes(Map.of(CLAIM, List.of(""))));
String organizationId3 = organization3.getId();

// add organizanization Example 4 with a different attribute `another_attribute`
var organization4 = createOrganization(
new OrganizationRepresentation()
.name("example4")
.domains(List.of("example4.com"))
.url("www.example4.com")
.displayName("Example 4")
.attributes(Map.of(SECOND_CLAIM, List.of("My Value"))));
String organizationId4 = organization4.getId();

// add the user to all organization
final UserRepresentation user = createUserWithCredentials(keycloak, REALM, "jdoe", "pass");
List<String> organizationIdList = Arrays.asList(organizationId1, organizationId2, organizationId3, organizationId4);
for (String organizationId : organizationIdList) {
Response response = putRequest("foo", organizationId, "members", user.getId());
assertThat(response.getStatusCode(), is(Status.CREATED.getStatusCode()));
}

RealmResource realm = keycloak.realm(REALM);
ClientRepresentation client = realm.clients().findByClientId(ADMIN_CLI).get(0);

// parse the received access-token
configureCustomOidcProtocolMapper(realm, client);

keycloak = getKeycloak(REALM, ADMIN_CLI, user.getUsername(), "pass");

TokenVerifier<AccessToken> verifier = TokenVerifier.create(keycloak.tokenManager().getAccessTokenString(),
AccessToken.class);
verifier.parse();

// check for the custom claim
AccessToken accessToken = verifier.getToken();
validateFirstClaim(accessToken, organizationId1, organizationId2, organizationId3, organizationId4);
validateSecondClaim(accessToken, organizationId4);

// change authorization
keycloak = getKeycloak(REALM, ADMIN_CLI, container.getAdminUsername(), container.getAdminPassword());
// delete user
deleteUser(keycloak, REALM, user.getId());

// delete organization
for (String organizationId : organizationIdList) {
deleteOrganization(keycloak, organizationId);
}
}

private void validateSecondClaim(AccessToken accessToken, String organizationId4) {
Map<String, Object> customClaimValue = (Map<String, Object>) accessToken.getOtherClaims().get(SECOND_CLAIM);
log.debugf("Custom Claim name secret_attr= %s", customClaimValue.toString());

// check the attribute values
assertNotNull(customClaimValue);

// validate organization Example 4 value value is "My Second Secret Value"
assertThat(customClaimValue.containsKey(organizationId4), is(true));
assertEquals("My Value", customClaimValue.get(organizationId4));
}

private void validateFirstClaim(AccessToken accessToken, String organizationId1, String organizationId2,
String organizationId3, String organizationId4) {
Map<String, Object> customClaimValue = (Map<String, Object>) accessToken.getOtherClaims().get(CLAIM);
log.debugf("Custom Claim name secret_attr= %s", customClaimValue.toString());

// check the attribute values
assertNotNull(customClaimValue);

// validate organization Example 1 value is "My Secret Value"
assertThat(customClaimValue.containsKey(organizationId1), is(true));
assertEquals("My Secret Value", customClaimValue.get(organizationId1));

// validate organization Example 2 value value is "My Second Secret Value"
assertThat(customClaimValue.containsKey(organizationId2), is(true));
assertEquals("My Second Secret Value", customClaimValue.get(organizationId2));

// validate organization Example 3 value is empty string
assertThat(customClaimValue.containsKey(organizationId3), is(true));
assertEquals("", customClaimValue.get(organizationId3));

// validate organization Example 4 value is not available
assertThat(customClaimValue.containsKey(organizationId4), is(false));
}

private static void configureCustomOidcProtocolMapper(RealmResource realm, ClientRepresentation client) {

// add first claim is same as attribute name
ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
mapper.setProtocolMapper(OrganizationSpecificAttributeMapper.PROVIDER_ID);
mapper.setName(CLAIM);
Map<String, String> config = new HashMap<>();
config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, CLAIM);
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
mapper.setConfig(config);
realm.clients().get(client.getId()).getProtocolMappers().createMapper(mapper).close();

// add second claim is same as attribute name
mapper = new ProtocolMapperRepresentation();
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
mapper.setProtocolMapper(OrganizationSpecificAttributeMapper.PROVIDER_ID);
mapper.setName(SECOND_CLAIM);
config = new HashMap<>();
config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, SECOND_CLAIM);
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
mapper.setConfig(config);
realm.clients().get(client.getId()).getProtocolMappers().createMapper(mapper).close();
}
}

0 comments on commit eb1ac15

Please sign in to comment.