KEYCLOAK-10251 New Claim JSON Type - JSON
This commit is contained in:
parent
c883c11e7e
commit
0376e7241a
7 changed files with 78 additions and 16 deletions
|
@ -26,7 +26,8 @@ public class ClaimTypeModel {
|
|||
public static enum ValueType {
|
||||
BOOLEAN,
|
||||
INT,
|
||||
STRING
|
||||
STRING,
|
||||
JSON
|
||||
}
|
||||
|
||||
private String id;
|
||||
|
|
|
@ -25,12 +25,10 @@ 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;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -122,6 +120,11 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
|
|||
for (Object val : list) {
|
||||
if (valueEquals(desiredValue, val)) return true;
|
||||
}
|
||||
} else if (value instanceof JsonNode) {
|
||||
try {
|
||||
if (JsonSerialization.readValue(desiredValue, JsonNode.class).equals(value)) return true;
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -224,9 +224,11 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
|
|||
}
|
||||
return arrayIndex == idx? values : null;
|
||||
}
|
||||
if (!currentNode.isValueNode() || currentNode.isNull()) {
|
||||
logger.debug("JsonNode is not value node for name " + currentFieldName);
|
||||
if (currentNode.isNull()) {
|
||||
logger.debug("JsonNode is null node for name " + currentFieldName);
|
||||
return null;
|
||||
} else if (!currentNode.isValueNode()) {
|
||||
return currentNode;
|
||||
}
|
||||
String ret = currentNode.asText();
|
||||
if (ret != null && !ret.trim().isEmpty())
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.protocol.oidc.mappers;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||
|
@ -24,6 +26,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
@ -117,6 +120,13 @@ public class OIDCAttributeMapperHelper {
|
|||
return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger);
|
||||
}
|
||||
throw new RuntimeException("cannot map type for token claim");
|
||||
case "JSON":
|
||||
JsonNode jsonNodeObject = getJsonNode(attributeValue);
|
||||
if (jsonNodeObject != null) return jsonNodeObject;
|
||||
if (attributeValue instanceof List) {
|
||||
return transform((List<JsonNode>) attributeValue, OIDCAttributeMapperHelper::getJsonNode);
|
||||
}
|
||||
throw new RuntimeException("cannot map type for token claim");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -144,6 +154,17 @@ public class OIDCAttributeMapperHelper {
|
|||
if (attributeValue instanceof String) return Boolean.valueOf((String) attributeValue);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonNode getJsonNode(Object attributeValue) {
|
||||
if (attributeValue instanceof JsonNode) return (JsonNode) attributeValue;
|
||||
if (attributeValue instanceof String) {
|
||||
try {
|
||||
return JsonSerialization.readValue(attributeValue.toString(), JsonNode.class);
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
@ -267,11 +288,12 @@ public class OIDCAttributeMapperHelper {
|
|||
ProviderConfigProperty property = new ProviderConfigProperty();
|
||||
property.setName(JSON_TYPE);
|
||||
property.setLabel(JSON_TYPE);
|
||||
List<String> types = new ArrayList(3);
|
||||
List<String> types = new ArrayList(5);
|
||||
types.add("String");
|
||||
types.add("long");
|
||||
types.add("int");
|
||||
types.add("boolean");
|
||||
types.add("JSON");
|
||||
property.setType(ProviderConfigProperty.LIST_TYPE);
|
||||
property.setOptions(types);
|
||||
property.setHelpText(JSON_TYPE_TOOLTIP);
|
||||
|
|
|
@ -73,14 +73,25 @@ public class AbstractJsonUserAttributeMapperTest {
|
|||
|
||||
@Test
|
||||
public void getJsonValue_nestedSimpleValues() throws JsonProcessingException, IOException {
|
||||
// JsonNode if path points to JSON object
|
||||
Assert.assertEquals(mapper.readTree("{\n"
|
||||
+ " \"value1\": \" fgh \",\n"
|
||||
+ " \"value_null\" : null,\n"
|
||||
+ " \"value_empty\" : \"\",\n"
|
||||
+ " \"nest2\":{\n"
|
||||
+ " \"value_b\" : false,\n"
|
||||
+ " \"value_i\" : 43\n"
|
||||
+ " }\n"
|
||||
+ " }"), AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1"));
|
||||
Assert.assertEquals(mapper.readTree("{\n"
|
||||
+ " \"value_b\" : false,\n"
|
||||
+ " \"value_i\" : 43\n"
|
||||
+ " }"), AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2"));
|
||||
Assert.assertEquals(mapper.readTree("{\"av1\": \"vala1\"}"), AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0]"));
|
||||
|
||||
// null if path points to JSON object
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2"));
|
||||
|
||||
//unknown field returns null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_unknown"));
|
||||
//unknown field returns null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_unknown"));
|
||||
|
||||
// we check value is trimmed also!
|
||||
Assert.assertEquals("fgh", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value1"));
|
||||
|
@ -123,7 +134,6 @@ public class AbstractJsonUserAttributeMapperTest {
|
|||
|
||||
//different path erros or nonexisting indexes or fields return null
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[2].av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0]"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0].av_unknown"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[].av1"));
|
||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a"));
|
||||
|
|
|
@ -54,6 +54,7 @@ import javax.ws.rs.core.Response;
|
|||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -139,6 +140,8 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
|||
user.singleAttribute("country", "USA");
|
||||
user.singleAttribute("formatted", "6 Foo Street");
|
||||
user.singleAttribute("phone", "617-777-6666");
|
||||
user.singleAttribute("json-attribute", "{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}");
|
||||
user.getAttributes().put("json-attribute-multi", Arrays.asList("{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}", "{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}"));
|
||||
|
||||
List<String> departments = Arrays.asList("finance", "development");
|
||||
user.getAttributes().put("departments", departments);
|
||||
|
@ -165,6 +168,10 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
|||
app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "realm-user")).close();
|
||||
app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper1","computed-via-script", "computed-via-script", "String", true, true, "'hello_' + user.username", false)).close();
|
||||
app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper2","multiValued-via-script", "multiValued-via-script", "String", true, true, "new java.util.ArrayList(['A','B'])", true)).close();
|
||||
app.getProtocolMappers().createMapper(createClaimMapper("json-attribute-mapper", "json-attribute", "claim-from-json-attribute",
|
||||
"JSON", true, true, false)).close();
|
||||
app.getProtocolMappers().createMapper(createClaimMapper("json-attribute-mapper-multi", "json-attribute-multi", "claim-from-json-attribute-multi",
|
||||
"JSON", true, true, true)).close();
|
||||
|
||||
Response response = app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper3", "syntax-error-script", "syntax-error-script", "String", true, true, "func_tion foo(){ return 'fail';} foo()", false));
|
||||
assertThat(response.getStatusInfo().getFamily(), is(Response.Status.Family.CLIENT_ERROR));
|
||||
|
@ -200,6 +207,17 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
|||
assertThat(firstDepartment, instanceOf(String.class));
|
||||
assertThat(firstDepartment, anyOf(is("finance"), is("development"))); // Has to be the first item
|
||||
|
||||
Map jsonClaim = (Map) idToken.getOtherClaims().get("claim-from-json-attribute");
|
||||
assertThat(jsonClaim.get("a"), instanceOf(int.class));
|
||||
assertThat(jsonClaim.get("c"), instanceOf(Collection.class));
|
||||
assertThat(jsonClaim.get("d"), instanceOf(Map.class));
|
||||
|
||||
List<Map> jsonClaims = (List<Map>) idToken.getOtherClaims().get("claim-from-json-attribute-multi");
|
||||
assertEquals(2, jsonClaims.size());
|
||||
assertThat(jsonClaims.get(0).get("a"), instanceOf(int.class));
|
||||
assertThat(jsonClaims.get(1).get("c"), instanceOf(Collection.class));
|
||||
assertThat(jsonClaims.get(1).get("d"), instanceOf(Map.class));
|
||||
|
||||
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||
assertEquals(accessToken.getName(), "Tom Brady");
|
||||
assertNotNull(accessToken.getAddress());
|
||||
|
@ -233,6 +251,11 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
|||
// Assert allowed origins
|
||||
Assert.assertNames(accessToken.getAllowedOrigins(), "http://localhost:8180", "https://localhost:8543");
|
||||
|
||||
jsonClaim = (Map) accessToken.getOtherClaims().get("claim-from-json-attribute");
|
||||
assertThat(jsonClaim.get("a"), instanceOf(int.class));
|
||||
assertThat(jsonClaim.get("c"), instanceOf(Collection.class));
|
||||
assertThat(jsonClaim.get("d"), instanceOf(Map.class));
|
||||
|
||||
oauth.openLogout();
|
||||
}
|
||||
|
||||
|
@ -253,6 +276,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
|||
|| model.getName().equals("hard-realm")
|
||||
|| model.getName().equals("hard-app")
|
||||
|| model.getName().equals("test-script-mapper")
|
||||
|| model.getName().equals("json-attribute-mapper")
|
||||
) {
|
||||
app.getProtocolMappers().delete(model.getId());
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ selectRole.tooltip=Enter role in the textbox to the left, or click this button t
|
|||
tokenClaimName.label=Token Claim Name
|
||||
tokenClaimName.tooltip=Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\\.).
|
||||
jsonType.label=Claim JSON Type
|
||||
jsonType.tooltip=JSON type that should be used to populate the json claim in the token. long, int, boolean, and String are valid values.
|
||||
jsonType.tooltip=JSON type that should be used to populate the json claim in the token. long, int, boolean, String and JSON are valid values.
|
||||
includeInIdToken.label=Add to ID token
|
||||
includeInIdToken.tooltip=Should the claim be added to the ID token?
|
||||
includeInAccessToken.label=Add to access token
|
||||
|
|
Loading…
Reference in a new issue