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 {
|
public static enum ValueType {
|
||||||
BOOLEAN,
|
BOOLEAN,
|
||||||
INT,
|
INT,
|
||||||
STRING
|
STRING,
|
||||||
|
JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
|
|
|
@ -25,12 +25,10 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -122,6 +120,11 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
|
||||||
for (Object val : list) {
|
for (Object val : list) {
|
||||||
if (valueEquals(desiredValue, val)) return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,9 +224,11 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
|
||||||
}
|
}
|
||||||
return arrayIndex == idx? values : null;
|
return arrayIndex == idx? values : null;
|
||||||
}
|
}
|
||||||
if (!currentNode.isValueNode() || currentNode.isNull()) {
|
if (currentNode.isNull()) {
|
||||||
logger.debug("JsonNode is not value node for name " + currentFieldName);
|
logger.debug("JsonNode is null node for name " + currentFieldName);
|
||||||
return null;
|
return null;
|
||||||
|
} else if (!currentNode.isValueNode()) {
|
||||||
|
return currentNode;
|
||||||
}
|
}
|
||||||
String ret = currentNode.asText();
|
String ret = currentNode.asText();
|
||||||
if (ret != null && !ret.trim().isEmpty())
|
if (ret != null && !ret.trim().isEmpty())
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.mappers;
|
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.models.ProtocolMapperModel;
|
||||||
import org.keycloak.protocol.ProtocolMapper;
|
import org.keycloak.protocol.ProtocolMapper;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
|
@ -24,6 +26,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -117,6 +120,13 @@ public class OIDCAttributeMapperHelper {
|
||||||
return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger);
|
return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger);
|
||||||
}
|
}
|
||||||
throw new RuntimeException("cannot map type for token claim");
|
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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -145,6 +155,17 @@ public class OIDCAttributeMapperHelper {
|
||||||
return null;
|
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.)
|
// 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)
|
// or any character other than backslash (escaping) and dot (claim component separator)
|
||||||
private static final Pattern CLAIM_COMPONENT = Pattern.compile("^((\\\\.|[^\\\\.])+?)\\.");
|
private static final Pattern CLAIM_COMPONENT = Pattern.compile("^((\\\\.|[^\\\\.])+?)\\.");
|
||||||
|
@ -267,11 +288,12 @@ public class OIDCAttributeMapperHelper {
|
||||||
ProviderConfigProperty property = new ProviderConfigProperty();
|
ProviderConfigProperty property = new ProviderConfigProperty();
|
||||||
property.setName(JSON_TYPE);
|
property.setName(JSON_TYPE);
|
||||||
property.setLabel(JSON_TYPE);
|
property.setLabel(JSON_TYPE);
|
||||||
List<String> types = new ArrayList(3);
|
List<String> types = new ArrayList(5);
|
||||||
types.add("String");
|
types.add("String");
|
||||||
types.add("long");
|
types.add("long");
|
||||||
types.add("int");
|
types.add("int");
|
||||||
types.add("boolean");
|
types.add("boolean");
|
||||||
|
types.add("JSON");
|
||||||
property.setType(ProviderConfigProperty.LIST_TYPE);
|
property.setType(ProviderConfigProperty.LIST_TYPE);
|
||||||
property.setOptions(types);
|
property.setOptions(types);
|
||||||
property.setHelpText(JSON_TYPE_TOOLTIP);
|
property.setHelpText(JSON_TYPE_TOOLTIP);
|
||||||
|
|
|
@ -73,10 +73,21 @@ public class AbstractJsonUserAttributeMapperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getJsonValue_nestedSimpleValues() throws JsonProcessingException, IOException {
|
public void getJsonValue_nestedSimpleValues() throws JsonProcessingException, IOException {
|
||||||
|
// JsonNode if path points to JSON object
|
||||||
// null if path points to JSON object
|
Assert.assertEquals(mapper.readTree("{\n"
|
||||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1"));
|
+ " \"value1\": \" fgh \",\n"
|
||||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2"));
|
+ " \"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]"));
|
||||||
|
|
||||||
//unknown field returns null
|
//unknown field returns null
|
||||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown"));
|
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown"));
|
||||||
|
@ -123,7 +134,6 @@ public class AbstractJsonUserAttributeMapperTest {
|
||||||
|
|
||||||
//different path erros or nonexisting indexes or fields return null
|
//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[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[0].av_unknown"));
|
||||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[].av1"));
|
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[].av1"));
|
||||||
Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a"));
|
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.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -139,6 +140,8 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
||||||
user.singleAttribute("country", "USA");
|
user.singleAttribute("country", "USA");
|
||||||
user.singleAttribute("formatted", "6 Foo Street");
|
user.singleAttribute("formatted", "6 Foo Street");
|
||||||
user.singleAttribute("phone", "617-777-6666");
|
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");
|
List<String> departments = Arrays.asList("finance", "development");
|
||||||
user.getAttributes().put("departments", departments);
|
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(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-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(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));
|
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));
|
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, instanceOf(String.class));
|
||||||
assertThat(firstDepartment, anyOf(is("finance"), is("development"))); // Has to be the first item
|
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());
|
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||||
assertEquals(accessToken.getName(), "Tom Brady");
|
assertEquals(accessToken.getName(), "Tom Brady");
|
||||||
assertNotNull(accessToken.getAddress());
|
assertNotNull(accessToken.getAddress());
|
||||||
|
@ -233,6 +251,11 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
||||||
// Assert allowed origins
|
// Assert allowed origins
|
||||||
Assert.assertNames(accessToken.getAllowedOrigins(), "http://localhost:8180", "https://localhost:8543");
|
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();
|
oauth.openLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +276,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|
||||||
|| model.getName().equals("hard-realm")
|
|| model.getName().equals("hard-realm")
|
||||||
|| model.getName().equals("hard-app")
|
|| model.getName().equals("hard-app")
|
||||||
|| model.getName().equals("test-script-mapper")
|
|| model.getName().equals("test-script-mapper")
|
||||||
|
|| model.getName().equals("json-attribute-mapper")
|
||||||
) {
|
) {
|
||||||
app.getProtocolMappers().delete(model.getId());
|
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.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 (\\.).
|
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.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.label=Add to ID token
|
||||||
includeInIdToken.tooltip=Should the claim be added to the ID token?
|
includeInIdToken.tooltip=Should the claim be added to the ID token?
|
||||||
includeInAccessToken.label=Add to access token
|
includeInAccessToken.label=Add to access token
|
||||||
|
|
Loading…
Reference in a new issue