KEYCLOAK-6577 KEYCLOAK-5609 Support dot in claim names by escaping with backslash

This commit is contained in:
Hynek Mlnarik 2018-07-16 18:53:32 +02:00 committed by Hynek Mlnařík
parent c8bc0d6d7b
commit b43392bac8
13 changed files with 189 additions and 18 deletions

View file

@ -173,6 +173,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>

View file

@ -23,10 +23,14 @@ 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 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>
@ -37,13 +41,16 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
public static final String CLAIM_VALUE = "claim.value";
public static Object getClaimValue(JsonWebToken token, String claim) {
String[] split = claim.split("\\.");
List<String> split = OIDCAttributeMapperHelper.splitClaimPath(claim);
Map<String, Object> jsonObject = token.getOtherClaims();
for (int i = 0; i < split.length; i++) {
if (i == split.length - 1) {
return jsonObject.get(split[i]);
final int length = split.size();
int i = 0;
for (String component : split) {
i++;
if (i == length) {
return jsonObject.get(component);
} else {
Object val = jsonObject.get(split[i]);
Object val = jsonObject.get(component);
if (!(val instanceof Map)) return null;
jsonObject = (Map<String, Object>)val;
}

View file

@ -49,7 +49,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper {
property1 = new ProviderConfigProperty();
property1.setName(CLAIM);
property1.setLabel("Claim");
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
property1.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property1);
property1 = new ProviderConfigProperty();

View file

@ -56,7 +56,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
property1 = new ProviderConfigProperty();
property1.setName(CLAIM);
property1.setLabel("Claim");
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'.");
property1.setHelpText("Name of claim to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
property1.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property1);
property = new ProviderConfigProperty();

View file

@ -27,6 +27,8 @@ import org.keycloak.services.ServicesLogger;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@ -143,6 +145,28 @@ 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<String> splitClaimPath(String claimPath) {
final LinkedList<String> 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) {
attributeValue = mapAttributeValue(mappingModel, attributeValue);
if (attributeValue == null) return;
@ -151,17 +175,20 @@ public class OIDCAttributeMapperHelper {
if (protocolClaim == null) {
return;
}
String[] split = protocolClaim.split("\\.");
List<String> split = splitClaimPath(protocolClaim);
final int length = split.size();
int i = 0;
Map<String, Object> jsonObject = token.getOtherClaims();
for (int i = 0; i < split.length; i++) {
if (i == split.length - 1) {
jsonObject.put(split[i], attributeValue);
for (String component : split) {
i++;
if (i == length) {
jsonObject.put(component, attributeValue);
} else {
Map<String, Object> nested = (Map<String, Object>)jsonObject.get(split[i]);
Map<String, Object> nested = (Map<String, Object>)jsonObject.get(component);
if (nested == null) {
nested = new HashMap<String, Object>();
jsonObject.put(split[i], nested);
jsonObject.put(component, nested);
}
jsonObject = nested;

View file

@ -0,0 +1,44 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.mappers;
import org.hamcrest.Matchers;
import org.junit.Test;
import static org.junit.Assert.assertThat;
/**
*
* @author hmlnarik
*/
public class OIDCAttributeMapperHelperTest {
@Test
public void testSplitClaimPath() {
assertThat(OIDCAttributeMapperHelper.splitClaimPath(""), Matchers.empty());
assertThat(OIDCAttributeMapperHelper.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(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"));
}
}

View file

@ -37,6 +37,8 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
private static final Set<String> PROTECTED_NAMES = ImmutableSet.<String>builder().add("email").add("lastName").add("firstName").build();
private static final Map<String, String> ATTRIBUTE_NAME_TRANSLATION = ImmutableMap.<String, String>builder()
.put("dotted.email", "dotted.email")
.put("nested.email", "nested.email")
.put(ATTRIBUTE_TO_MAP_FRIENDLY_NAME, MAPPED_ATTRIBUTE_FRIENDLY_NAME)
.put(ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME)
.build();
@ -198,9 +200,13 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker
public void testBasicMappingEmail() {
testValueMapping(ImmutableMap.<String, List<String>>builder()
.put("email", ImmutableList.<String>builder().add(bc.getUserEmail()).build())
.put("nested.email", ImmutableList.<String>builder().add(bc.getUserEmail()).build())
.put("dotted.email", ImmutableList.<String>builder().add(bc.getUserEmail()).build())
.build(),
ImmutableMap.<String, List<String>>builder()
.put("email", ImmutableList.<String>builder().add("other_email@redhat.com").build())
.put("nested.email", ImmutableList.<String>builder().add("other_email@redhat.com").build())
.put("dotted.email", ImmutableList.<String>builder().add("other_email@redhat.com").build())
.build()
);
}

View file

@ -74,6 +74,32 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
emailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
emailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
ProtocolMapperRepresentation nestedAttrMapper = new ProtocolMapperRepresentation();
nestedAttrMapper.setName("attribute - nested claim");
nestedAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
nestedAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
Map<String, String> nestedEmailMapperConfig = nestedAttrMapper.getConfig();
nestedEmailMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, "nested.email");
nestedEmailMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "nested.email");
nestedEmailMapperConfig.put(OIDCAttributeMapperHelper.JSON_TYPE, ProviderConfigProperty.STRING_TYPE);
nestedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
nestedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
nestedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
ProtocolMapperRepresentation dottedAttrMapper = new ProtocolMapperRepresentation();
dottedAttrMapper.setName("attribute - claim with dot in name");
dottedAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
dottedAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
Map<String, String> dottedEmailMapperConfig = dottedAttrMapper.getConfig();
dottedEmailMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, "dotted.email");
dottedEmailMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "dotted\\.email");
dottedEmailMapperConfig.put(OIDCAttributeMapperHelper.JSON_TYPE, ProviderConfigProperty.STRING_TYPE);
dottedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
dottedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
dottedEmailMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
ProtocolMapperRepresentation userAttrMapper = new ProtocolMapperRepresentation();
userAttrMapper.setName("attribute - name");
userAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
@ -88,7 +114,7 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "true");
client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper));
client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, nestedAttrMapper, dottedAttrMapper));
return Collections.singletonList(client);
}

View file

@ -97,6 +97,26 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
emailMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri");
emailMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, "email");
ProtocolMapperRepresentation dottedAttrMapper = new ProtocolMapperRepresentation();
dottedAttrMapper.setName("email - dotted");
dottedAttrMapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
dottedAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID);
Map<String, String> dottedEmailMapperConfig = dottedAttrMapper.getConfig();
dottedEmailMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, "dotted.email");
dottedEmailMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "dotted.email");
dottedEmailMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri");
ProtocolMapperRepresentation nestedAttrMapper = new ProtocolMapperRepresentation();
nestedAttrMapper.setName("email - nested");
nestedAttrMapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
nestedAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID);
Map<String, String> nestedEmailMapperConfig = nestedAttrMapper.getConfig();
nestedEmailMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, "nested.email");
nestedEmailMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "nested.email");
nestedEmailMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, "urn:oasis:names:tc:SAML:2.0:attrname-format:uri");
ProtocolMapperRepresentation userAttrMapper = new ProtocolMapperRepresentation();
userAttrMapper.setName("attribute - name");
userAttrMapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
@ -119,7 +139,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC);
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME);
client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, userFriendlyAttrMapper));
client.setProtocolMappers(Arrays.asList(emailMapper, dottedAttrMapper, nestedAttrMapper, userAttrMapper, userFriendlyAttrMapper));
return client;
}

View file

@ -32,7 +32,23 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest
.put(UserAttributeMapper.USER_ATTRIBUTE, "email")
.build());
return Lists.newArrayList(attrMapper1, emailAttrMapper);
IdentityProviderMapperRepresentation nestedEmailAttrMapper = new IdentityProviderMapperRepresentation();
nestedEmailAttrMapper.setName("nested-attribute-mapper-email");
nestedEmailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
nestedEmailAttrMapper.setConfig(ImmutableMap.<String,String>builder()
.put(UserAttributeMapper.CLAIM, "nested.email")
.put(UserAttributeMapper.USER_ATTRIBUTE, "nested.email")
.build());
IdentityProviderMapperRepresentation dottedEmailAttrMapper = new IdentityProviderMapperRepresentation();
dottedEmailAttrMapper.setName("dotted-attribute-mapper-email");
dottedEmailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
dottedEmailAttrMapper.setConfig(ImmutableMap.<String,String>builder()
.put(UserAttributeMapper.CLAIM, "dotted\\.email")
.put(UserAttributeMapper.USER_ATTRIBUTE, "dotted.email")
.build());
return Lists.newArrayList(attrMapper1, emailAttrMapper, nestedEmailAttrMapper, dottedEmailAttrMapper);
}
}

View file

@ -24,6 +24,22 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest
.put(UserAttributeMapper.USER_ATTRIBUTE, "email")
.build());
IdentityProviderMapperRepresentation attrMapperNestedEmail = new IdentityProviderMapperRepresentation();
attrMapperNestedEmail.setName("nested-attribute-mapper-email");
attrMapperNestedEmail.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
attrMapperNestedEmail.setConfig(ImmutableMap.<String,String>builder()
.put(UserAttributeMapper.ATTRIBUTE_NAME, "nested.email")
.put(UserAttributeMapper.USER_ATTRIBUTE, "nested.email")
.build());
IdentityProviderMapperRepresentation attrMapperDottedEmail = new IdentityProviderMapperRepresentation();
attrMapperDottedEmail.setName("dotted-attribute-mapper-email");
attrMapperDottedEmail.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
attrMapperDottedEmail.setConfig(ImmutableMap.<String,String>builder()
.put(UserAttributeMapper.ATTRIBUTE_NAME, "dotted.email")
.put(UserAttributeMapper.USER_ATTRIBUTE, "dotted.email")
.build());
IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation();
attrMapper1.setName("attribute-mapper");
attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID);
@ -40,7 +56,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest
.put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_FRIENDLY_NAME)
.build());
return Lists.newArrayList(attrMapperEmail, attrMapper1, attrMapper2);
return Lists.newArrayList(attrMapperEmail, attrMapper1, attrMapper2, attrMapperDottedEmail, attrMapperNestedEmail);
}
}

View file

@ -143,6 +143,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
app.getProtocolMappers().createMapper(createHardcodedClaim("hard-nested", "nested.hard", "coded-nested", "String", true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("custom phone", "phone", "home_phone", "String", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("nested phone", "phone", "home.phone", "String", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("dotted phone", "phone", "home\\.phone", "String", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("departments", "departments", "department", "String", true, true, true)).close();
app.getProtocolMappers().createMapper(createClaimMapper("firstDepartment", "departments", "firstDepartment", "String", true, true, false)).close();
app.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded")).close();
@ -170,6 +171,8 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
assertEquals(idToken.getAddress().getFormattedAddress(), "6 Foo Street");
assertNotNull(idToken.getOtherClaims().get("home_phone"));
assertThat((List<String>) idToken.getOtherClaims().get("home_phone"), hasItems("617-777-6666"));
assertNotNull(idToken.getOtherClaims().get("home.phone"));
assertThat((List<String>) idToken.getOtherClaims().get("home.phone"), hasItems("617-777-6666"));
assertEquals("coded", idToken.getOtherClaims().get("hard"));
Map nested = (Map) idToken.getOtherClaims().get("nested");
assertEquals("coded-nested", nested.get("hard"));
@ -221,6 +224,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
|| model.getName().equals("hard")
|| model.getName().equals("hard-nested")
|| model.getName().equals("custom phone")
|| model.getName().equals("dotted phone")
|| model.getName().equals("departments")
|| model.getName().equals("firstDepartment")
|| model.getName().equals("nested phone")

View file

@ -202,7 +202,7 @@ multivalued.tooltip=Indicates if attribute supports multiple values. If true, th
selectRole.label=Select Role
selectRole.tooltip=Enter role in the textbox to the left, or click this button to browse and select the role you want.
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.
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.
includeInIdToken.label=Add to ID token