keycloak-memoizeit
Changes
services/pom.xml 5(+5 -0)
services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java 44(+44 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java 6(+6 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java 28(+27 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java 22(+21 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java 18(+17 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java 18(+17 -1)
Details
services/pom.xml 5(+5 -0)
diff --git a/services/pom.xml b/services/pom.xml
index 15233a6..77f3597 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -174,6 +174,11 @@
             <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>
             <scope>test</scope>
                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 f2b5288..a5bbbbe 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
@@ -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;
             }
                diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java
index e026623..dcb1622 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java
@@ -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();
                diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
index 2e2abca..4770cf3 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
@@ -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();
                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 0365bc3..933654c 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
@@ -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;
                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
new file mode 100644
index 0000000..438544c
--- /dev/null
+++ b/services/src/test/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelperTest.java
@@ -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"));
+    }
+}
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
index 045aeb9..4f3b29e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java
@@ -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()
         );
     }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
index ce92201..c6da797 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java
@@ -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);
     }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
index eabeec8..904acd6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java
@@ -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;
     }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java
index 0927a86..b5c3063 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java
@@ -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);
     }
 
 }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java
index 7184ef8..7d01ef7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java
@@ -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);
     }
 
 }
                diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
index f3e8ade..4e19606 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java
@@ -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")
                diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 7572350..4924faa 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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