claimComponents = new LinkedList<>();
+ Matcher m = CLAIM_COMPONENT.matcher(claim);
+ int start = 0;
+ while (m.find()) {
+ claimComponents.add(BACKSLASHED_CHARACTER.matcher(m.group(1)).replaceAll("$1"));
+ start = m.end();
+ // This is necessary to match the start of region as the start of string as determined by ^
+ m.region(start, claim.length());
+ }
+ if (claim.length() > start) {
+ claimComponents.add(BACKSLASHED_CHARACTER.matcher(claim.substring(start)).replaceAll("$1"));
+ }
+ return claimComponents;
+ }
+
+ /**
+ * Determines if the given {@code claim} contains paths.
+ *
+ * @param claim the claim
+ * @return {@code true} if the {@code claim} contains paths. Otherwise, false.
+ */
+ public static boolean hasPath(String claim) {
+ return CLAIM_COMPONENT.matcher(claim).find();
+ }
+
+ /**
+ * Returns the value corresponding to the given {@code claim}.
+ *
+ * @param node the JSON node
+ * @param claim the claim
+ * @return the value
+ */
+ public static Object getJsonValue(JsonNode node, String claim) {
+ if (node != null) {
+ List fields = splitClaimPath(claim);
+ if (fields.isEmpty() || claim.endsWith(".")) {
+ return null;
+ }
+
+ JsonNode currentNode = node;
+ for (String currentFieldName : fields) {
+
+ // if array path, retrieve field name and index
+ String currentNodeName = currentFieldName;
+ int arrayIndex = -1;
+ if (currentFieldName.endsWith("]")) {
+ int bi = currentFieldName.indexOf("[");
+ if (bi == -1) {
+ return null;
+ }
+ try {
+ String is = currentFieldName.substring(bi + 1, currentFieldName.length() - 1).trim();
+ arrayIndex = Integer.parseInt(is);
+ if( arrayIndex < 0) throw new ArrayIndexOutOfBoundsException();
+ } catch (Exception e) {
+ return null;
+ }
+ currentNodeName = currentFieldName.substring(0, bi).trim();
+ }
+
+ currentNode = currentNode.get(currentNodeName);
+ if (arrayIndex > -1 && currentNode.isArray()) {
+ currentNode = currentNode.get(arrayIndex);
+ }
+
+ if (currentNode == null) {
+ return null;
+ }
+
+ if (currentNode.isArray()) {
+ List values = new ArrayList<>();
+ for (JsonNode childNode : currentNode) {
+ if (childNode.isTextual()) {
+ values.add(childNode.textValue());
+ }
+ }
+ if (values.isEmpty()) {
+ return null;
+ }
+ return values ;
+ } else if (currentNode.isNull()) {
+ return null;
+ } else if (currentNode.isValueNode()) {
+ String ret = currentNode.asText();
+ if (ret != null && !ret.trim().isEmpty())
+ return ret.trim();
+ else
+ return null;
+ }
+
+ }
+ return currentNode;
+ }
+ return null;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
index 5db71a2da0..73e277f353 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
@@ -195,6 +195,8 @@ public class KeycloakIdentity implements Identity {
while (valueIterator.hasNext()) {
values.add(valueIterator.next().asText());
}
+ } else if (fieldValue.isObject()) {
+ values.add(fieldValue.toString());
} else {
String value = fieldValue.asText();
diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
index 05167f3eb4..ef2572aa7d 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
@@ -17,13 +17,14 @@
package org.keycloak.broker.oidc.mappers;
+import static org.keycloak.utils.JsonUtils.splitClaimPath;
+
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderMapperModel;
-import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.JsonSerialization;
@@ -47,7 +48,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
// found no match, try other claims
}
- List split = OIDCAttributeMapperHelper.splitClaimPath(claim);
+ List split = splitClaimPath(claim);
Map jsonObject = token.getOtherClaims();
final int length = split.size();
int i = 0;
diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
index ca0ddff039..615cb0d322 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
@@ -17,6 +17,8 @@
package org.keycloak.broker.oidc.mappers;
+import static org.keycloak.utils.JsonUtils.splitClaimPath;
+
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
@@ -27,7 +29,6 @@ import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
-import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
@@ -206,7 +207,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
if (baseNode != null) {
- List fields = OIDCAttributeMapperHelper.splitClaimPath(fieldPath);
+ List fields = splitClaimPath(fieldPath);
if (fields.isEmpty() || fieldPath.endsWith(".")) {
logger.debug("JSON path is invalid " + fieldPath);
return null;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
index 2fc96ef3a1..b59dbb631f 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
@@ -17,6 +17,8 @@
package org.keycloak.protocol.oidc.mappers;
+import static org.keycloak.utils.JsonUtils.splitClaimPath;
+
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.representations.AccessToken;
@@ -100,7 +102,7 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper
protocolClaim = CLIENT_ID_PATTERN.matcher(protocolClaim).replaceAll(clientId);
}
- List split = OIDCAttributeMapperHelper.splitClaimPath(protocolClaim);
+ List split = splitClaimPath(protocolClaim);
// Special case
if (checkAccessToken(token, split, attributeValue)) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
index 666b3ec8a8..2375e5e6ed 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java
@@ -17,6 +17,8 @@
package org.keycloak.protocol.oidc.mappers;
+import static org.keycloak.utils.JsonUtils.splitClaimPath;
+
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
import org.keycloak.models.ProtocolMapperModel;
@@ -31,8 +33,6 @@ import org.keycloak.util.JsonSerialization;
import java.util.*;
import java.util.function.Function;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -250,28 +250,6 @@ public class OIDCAttributeMapperHelper {
return null;
}
- // A character in a claim component is either a literal character escaped by a backslash (\., \\, \_, \q, etc.)
- // or any character other than backslash (escaping) and dot (claim component separator)
- private static final Pattern CLAIM_COMPONENT = Pattern.compile("^((\\\\.|[^\\\\.])+?)\\.");
-
- private static final Pattern BACKSLASHED_CHARACTER = Pattern.compile("\\\\(.)");
-
- public static List splitClaimPath(String claimPath) {
- final LinkedList claimComponents = new LinkedList<>();
- Matcher m = CLAIM_COMPONENT.matcher(claimPath);
- int start = 0;
- while (m.find()) {
- claimComponents.add(BACKSLASHED_CHARACTER.matcher(m.group(1)).replaceAll("$1"));
- start = m.end();
- // This is necessary to match the start of region as the start of string as determined by ^
- m.region(start, claimPath.length());
- }
- if (claimPath.length() > start) {
- claimComponents.add(BACKSLASHED_CHARACTER.matcher(claimPath.substring(start)).replaceAll("$1"));
- }
- return claimComponents;
- }
-
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
mapClaim(token, mappingModel, attributeValue, tokenPropertySetters, token.getOtherClaims());
}
diff --git a/services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java b/services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java
index 438544c16a..da1f80753b 100644
--- a/services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java
+++ b/services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java
@@ -18,6 +18,8 @@ package org.keycloak.protocol.oidc.mappers;
import org.hamcrest.Matchers;
import org.junit.Test;
+import org.keycloak.utils.JsonUtils;
+
import static org.junit.Assert.assertThat;
/**
@@ -28,17 +30,17 @@ public class OIDCAttributeMapperHelperTest {
@Test
public void testSplitClaimPath() {
- assertThat(OIDCAttributeMapperHelper.splitClaimPath(""), Matchers.empty());
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("a"), Matchers.contains("a"));
+ assertThat(JsonUtils.splitClaimPath(""), Matchers.empty());
+ assertThat(JsonUtils.splitClaimPath("a"), Matchers.contains("a"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("a.b"), Matchers.contains("a", "b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\.b"), Matchers.contains("a.b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\\\.b"), Matchers.contains("a\\", "b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\\\\\.b"), Matchers.contains("a\\.b"));
+ assertThat(JsonUtils.splitClaimPath("a.b"), Matchers.contains("a", "b"));
+ assertThat(JsonUtils.splitClaimPath("a\\.b"), Matchers.contains("a.b"));
+ assertThat(JsonUtils.splitClaimPath("a\\\\.b"), Matchers.contains("a\\", "b"));
+ assertThat(JsonUtils.splitClaimPath("a\\\\\\.b"), Matchers.contains("a\\.b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("c.a\\\\.b"), Matchers.contains("c", "a\\", "b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("c.a\\\\\\.b"), Matchers.contains("c", "a\\.b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("c\\\\\\.b.a\\\\\\.b"), Matchers.contains("c\\.b", "a\\.b"));
- assertThat(OIDCAttributeMapperHelper.splitClaimPath("c\\h\\.b.a\\\\\\.b"), Matchers.contains("ch.b", "a\\.b"));
+ assertThat(JsonUtils.splitClaimPath("c.a\\\\.b"), Matchers.contains("c", "a\\", "b"));
+ assertThat(JsonUtils.splitClaimPath("c.a\\\\\\.b"), Matchers.contains("c", "a\\.b"));
+ assertThat(JsonUtils.splitClaimPath("c\\\\\\.b.a\\\\\\.b"), Matchers.contains("c\\.b", "a\\.b"));
+ assertThat(JsonUtils.splitClaimPath("c\\h\\.b.a\\\\\\.b"), Matchers.contains("ch.b", "a\\.b"));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RegexPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RegexPolicyTest.java
index ab89c975f4..702da1160b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RegexPolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RegexPolicyTest.java
@@ -56,38 +56,35 @@ public class RegexPolicyTest extends AbstractAuthzTest {
@Override
public void addTestRealms(List testRealms) {
- ProtocolMapperRepresentation userAttrFooProtocolMapper = new ProtocolMapperRepresentation();
- userAttrFooProtocolMapper.setName("userAttrFoo");
- userAttrFooProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
- userAttrFooProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
- Map configFoo = new HashMap<>();
- configFoo.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
- configFoo.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
- configFoo.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
- configFoo.put("user.attribute", "foo");
- configFoo.put("claim.name", "foo");
- userAttrFooProtocolMapper.setConfig(configFoo);
+ Map claims = new HashMap<>();
- ProtocolMapperRepresentation userAttrBarProtocolMapper = new ProtocolMapperRepresentation();
- userAttrBarProtocolMapper.setName("userAttrBar");
- userAttrBarProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
- userAttrBarProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
- Map configBar = new HashMap<>();
- configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
- configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
- configBar.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
- configBar.put("user.attribute", "bar");
- configBar.put("claim.name", "bar");
- userAttrBarProtocolMapper.setConfig(configBar);
+ claims.put("user.attribute", "foo");
+ claims.put("claim.name", "foo");
+ ProtocolMapperRepresentation userAttrFooProtocolMapper = addClaimMapper("userAttrFoo", claims);
+
+ claims.put("user.attribute", "bar");
+ claims.put("claim.name", "bar");
+ ProtocolMapperRepresentation userAttrBarProtocolMapper = addClaimMapper("userAttrBar", claims);
+
+ claims.put("user.attribute", "json-simple");
+ claims.put("claim.name", "userinfo");
+ ProtocolMapperRepresentation userAttrJsonProtocolMapper = addClaimMapper("userAttrJsonSimple", claims);
+
+ claims.put("user.attribute", "json-complex");
+ claims.put("claim.name", "json-complex");
+ ProtocolMapperRepresentation userAttrJsonComplexProtocolMapper = addClaimMapper("userAttrJsonComplex", claims);
+
+ // For JSON-based claims, you can use dot notation for nesting and square brackets to access array fields by index. For example, contact.address[0].country.
testRealms.add(RealmBuilder.create().name("authz-test")
.user(UserBuilder.create().username("marta").password("password").addAttribute("foo", "foo").addAttribute("bar",
- "barbar"))
+ "barbar").addAttribute("json-simple", "{\"tenant\": \"abc\"}")
+ .addAttribute("json-complex", "{\"userinfo\": {\"tenant\": \"abc\"}, \"some-array\": [\"foo\",\"bar\"]}"))
.user(UserBuilder.create().username("taro").password("password").addAttribute("foo", "faa").addAttribute("bar",
"bbarbar"))
.client(ClientBuilder.create().clientId("resource-server-test").secret("secret").authorizationServicesEnabled(true)
.redirectUris("http://localhost/resource-server-test").directAccessGrants()
- .protocolMapper(userAttrFooProtocolMapper, userAttrBarProtocolMapper))
+ .protocolMapper(userAttrFooProtocolMapper, userAttrBarProtocolMapper, userAttrJsonProtocolMapper, userAttrJsonComplexProtocolMapper))
.build());
}
@@ -95,12 +92,21 @@ public class RegexPolicyTest extends AbstractAuthzTest {
public void configureAuthorization() throws Exception {
createResource("Resource A");
createResource("Resource B");
+ createResource("Resource C");
+ createResource("Resource D");
+ createResource("Resource E");
createRegexPolicy("Regex foo Policy", "foo", "foo");
createRegexPolicy("Regex bar Policy", "bar", "^bar.+$");
+ createRegexPolicy("Regex json-simple Policy", "userinfo.tenant", "^abc$");
+ createRegexPolicy("Regex json-complex Policy", "json-complex.userinfo.tenant", "^abc$");
+ createRegexPolicy("Regex json-array Policy", "json-complex.some-array[1]", "bar");
createResourcePermission("Resource A Permission", "Resource A", "Regex foo Policy");
createResourcePermission("Resource B Permission", "Resource B", "Regex bar Policy");
+ createResourcePermission("Resource C Permission", "Resource C", "Regex json-simple Policy");
+ createResourcePermission("Resource D Permission", "Resource D", "Regex json-complex Policy");
+ createResourcePermission("Resource E Permission", "Resource E", "Regex json-array Policy");
}
private void createResource(String name) {
@@ -164,6 +170,21 @@ public class RegexPolicyTest extends AbstractAuthzTest {
ticket = authzClient.protection().permission().create(request).getTicket();
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
assertNotNull(response.getToken());
+
+ request = new PermissionRequest("Resource C");
+ ticket = authzClient.protection().permission().create(request).getTicket();
+ response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+ assertNotNull(response.getToken());
+
+ request = new PermissionRequest("Resource D");
+ ticket = authzClient.protection().permission().create(request).getTicket();
+ response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+ assertNotNull(response.getToken());
+
+ request = new PermissionRequest("Resource E");
+ ticket = authzClient.protection().permission().create(request).getTicket();
+ response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+ assertNotNull(response.getToken());
}
@Test
@@ -188,9 +209,42 @@ public class RegexPolicyTest extends AbstractAuthzTest {
} catch (AuthorizationDeniedException ignore) {
}
+
+ // Access Resource C with taro.
+ request = new PermissionRequest("Resource C");
+ ticket = authzClient.protection().permission().create(request).getTicket();
+ try {
+ authzClient.authorization("taro", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail.");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
+
+ // Access Resource D with taro.
+ request = new PermissionRequest("Resource D");
+ ticket = authzClient.protection().permission().create(request).getTicket();
+ try {
+ authzClient.authorization("taro", "password").authorize(new AuthorizationRequest(ticket));
+ fail("Should fail.");
+ } catch (AuthorizationDeniedException ignore) {
+
+ }
}
private AuthzClient getAuthzClient() {
return AuthzClient.create(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"));
}
+
+ private ProtocolMapperRepresentation addClaimMapper(String name, Map claims) {
+ ProtocolMapperRepresentation userAttrBarProtocolMapper = new ProtocolMapperRepresentation();
+ userAttrBarProtocolMapper.setName(name);
+ userAttrBarProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
+ userAttrBarProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ Map configBar = new HashMap<>(claims);
+ configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+ configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+ configBar.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
+ userAttrBarProtocolMapper.setConfig(configBar);
+ return userAttrBarProtocolMapper;
+ }
}