keycloak-aplcache

KEYCLOAK-1487 Support for multiple values of one UserModel

6/27/2015 5:49:40 PM

Changes

Details

diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
index 9f5085e..6d3f8db 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java
@@ -110,7 +110,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
 
 		String value = getJsonValue(mapperModel, context);
 		if (value != null) {
-			user.setAttribute(attribute, value);
+			user.setSingleAttribute(attribute, value);
 		}
 	}
 
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
index 61500a7..d9cb079 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java
@@ -3,16 +3,14 @@ package org.keycloak.broker.oidc.mappers;
 import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
 import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
-import org.keycloak.broker.provider.IdentityBrokerException;
-import org.keycloak.models.ClientModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -76,7 +74,7 @@ public class UserAttributeMapper extends AbstractClaimMapper {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getClaimValue(mapperModel, context);
         if (value != null) {
-            user.setAttribute(attribute, value.toString());
+            user.setSingleAttribute(attribute, value.toString());
         }
     }
 
@@ -84,9 +82,9 @@ public class UserAttributeMapper extends AbstractClaimMapper {
     public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getClaimValue(mapperModel, context);
-        String current = user.getAttribute(attribute);
+        String current = user.getFirstAttribute(attribute);
         if (value != null && !value.equals(current)) {
-            user.setAttribute(attribute, value.toString());
+            user.setSingleAttribute(attribute, value.toString());
         } else if (value == null) {
             user.removeAttribute(attribute);
         }
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
index b5f9770..a1feac8 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java
@@ -2,17 +2,14 @@ package org.keycloak.broker.saml.mappers;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
-import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.saml.SAMLEndpoint;
 import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
 import org.keycloak.dom.saml.v2.assertion.AssertionType;
 import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
 import org.keycloak.dom.saml.v2.assertion.AttributeType;
-import org.keycloak.models.ClientModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
@@ -87,7 +84,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getAttribute(mapperModel, context);
         if (value != null) {
-            user.setAttribute(attribute, value.toString());
+            user.setSingleAttribute(attribute, value.toString());
         }
     }
 
@@ -115,9 +112,9 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper {
     public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
         String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE);
         Object value = getAttribute(mapperModel, context);
-        String current = user.getAttribute(attribute);
+        String current = user.getFirstAttribute(attribute);
         if (value != null && !value.equals(current)) {
-            user.setAttribute(attribute, value.toString());
+            user.setSingleAttribute(attribute, value.toString());
         } else if (value == null) {
             user.removeAttribute(attribute);
         }
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
index ff0311d..f5f0195 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml
@@ -2,6 +2,7 @@
 <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
     <changeSet author="bburke@redhat.com" id="1.4.0">
         <delete tableName="CLIENT_SESSION_AUTH_STATUS"/>
+        <delete tableName="CLIENT_SESSION_ROLE"/>
         <delete tableName="CLIENT_SESSION_PROT_MAPPER"/>
         <delete tableName="CLIENT_SESSION_NOTE"/>
         <delete tableName="CLIENT_SESSION"/>
@@ -22,6 +23,12 @@
                 <constraints nullable="true"/>
             </column>
         </addColumn>
+        <addColumn tableName="USER_ATTRIBUTE">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+
         <dropColumn tableName="AUTHENTICATOR"  columnName="PROVIDER_ID"/>
         <renameTable oldTableName="AUTHENTICATOR_CONFIG" newTableName="AUTHENTICATOR_CONFIG_ENTRY"/>
         <renameTable oldTableName="AUTHENTICATOR" newTableName="AUTHENTICATOR_CONFIG"/>
@@ -110,6 +117,8 @@
             </column>
         </createTable>
 
+        <dropPrimaryKey constraintName="CONSTRAINT_6" tableName="USER_ATTRIBUTE"/>
+        <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_USER_ATTRIBUTE_PK" tableName="USER_ATTRIBUTE"/>
         <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_REQ_ACT_PRV_PK" tableName="REQUIRED_ACTION_PROVIDER"/>
         <addPrimaryKey columnNames="REQUIRED_ACTION_ID, NAME" constraintName="CONSTRAINT_REQ_ACT_CFG_PK" tableName="REQUIRED_ACTION_CONFIG"/>
         <addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTR_CL_USR_SES_NOTE" tableName="CLIENT_USER_SESSION_NOTE"/>
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
index 094f506..30bd9f2 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/api/types/MapperContext.java
@@ -1,5 +1,6 @@
 package org.keycloak.connections.mongo.api.types;
 
+import java.lang.reflect.Type;
 import java.util.List;
 
 /**
@@ -14,9 +15,9 @@ public class MapperContext<T, S> {
     private final Class<? extends S> expectedReturnType;
 
     // in case that expected return type is generic type (like "List<String>"), then genericTypes could contain list of expected generic arguments
-    private final List<Class<?>> genericTypes;
+    private final List<Type> genericTypes;
 
-    public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Class<?>> genericTypes) {
+    public MapperContext(T objectToConvert, Class<? extends S> expectedReturnType, List<Type> genericTypes) {
         this.objectToConvert = objectToConvert;
         this.expectedReturnType = expectedReturnType;
         this.genericTypes = genericTypes;
@@ -30,7 +31,7 @@ public class MapperContext<T, S> {
         return expectedReturnType;
     }
 
-    public List<Class<?>> getGenericTypes() {
+    public List<Type> getGenericTypes() {
         return genericTypes;
     }
 }
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
index cc229c6..d435e1e 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListMapper.java
@@ -24,7 +24,7 @@ public class BasicDBListMapper implements Mapper<BasicDBList, List> {
     public List convertObject(MapperContext<BasicDBList, List> context) {
         BasicDBList dbList = context.getObjectToConvert();
         ArrayList<Object> appObjects = new ArrayList<Object>();
-        Class<?> expectedListElementType = context.getGenericTypes().get(0);
+        Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
 
         for (Object dbObject : dbList) {
             MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
index d43781a..eea2ee9 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBListToSetMapper.java
@@ -23,7 +23,7 @@ public class BasicDBListToSetMapper implements Mapper<BasicDBList, Set> {
     public Set convertObject(MapperContext<BasicDBList, Set> context) {
         BasicDBList dbList = context.getObjectToConvert();
         Set<Object> appObjects = new HashSet<Object>();
-        Class<?> expectedListElementType = context.getGenericTypes().get(0);
+        Class<?> expectedListElementType = (Class<?>) context.getGenericTypes().get(0);
 
         for (Object dbObject : dbList) {
             MapperContext<Object, Object> newContext = new MapperContext<Object, Object>(dbObject, expectedListElementType, null);
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
index 102cf21..e592df9 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/impl/types/BasicDBObjectMapper.java
@@ -14,6 +14,7 @@ import org.keycloak.util.reflections.Types;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -87,10 +88,14 @@ public class BasicDBObjectMapper<S> implements Mapper<BasicDBObject, S> {
             ParameterizedType parameterized = (ParameterizedType) type;
             Type[] genericTypeArguments = parameterized.getActualTypeArguments();
 
-            List<Class<?>> genericTypes = new ArrayList<Class<?>>();
-            for (Type genericType : genericTypeArguments) {
-                genericTypes.add((Class<?>)genericType);
-            }
+            List<Type> genericTypes = Arrays.asList(genericTypeArguments);
+            /*for (Type genericType : genericTypeArguments) {
+                if (genericType instanceof Class) {
+                    genericTypes.add((Class<?>) genericType);
+                } else {
+                    System.out.println("foo");
+                }
+            }*/
 
             Class<?> expectedReturnType = (Class<?>)parameterized.getRawType();
             context = new MapperContext<Object, Object>(valueFromDB, expectedReturnType, genericTypes);
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 747b64c..8607264 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -1,10 +1,14 @@
 package org.keycloak.representations.idm;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.codehaus.jackson.annotate.JsonIgnore;
+import org.keycloak.util.MultivaluedHashMap;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -21,7 +25,9 @@ public class UserRepresentation {
     protected String lastName;
     protected String email;
     protected String federationLink;
-    protected Map<String, String> attributes;
+
+    // Currently there is Map<String, List<String>> but for backwards compatibility, we also need to support Map<String, String>
+    protected Map<String, Object> attributes;
     protected List<CredentialRepresentation> credentials;
     protected List<String> requiredActions;
     protected List<FederatedIdentityRepresentation> federatedIdentities;
@@ -106,17 +112,23 @@ public class UserRepresentation {
         this.emailVerified = emailVerified;
     }
 
-    public Map<String, String> getAttributes() {
+    public Map<String, Object> getAttributes() {
         return attributes;
     }
 
-    public void setAttributes(Map<String, String> attributes) {
+    // This method can be removed once we can remove backwards compatibility with Keycloak 1.3 (then getAttributes() can be changed to return Map<String, List<String>> )
+    @JsonIgnore
+    public Map<String, List<String>> getAttributesAsListValues() {
+        return (Map) attributes;
+    }
+
+    public void setAttributes(Map<String, Object> attributes) {
         this.attributes = attributes;
     }
 
-    public UserRepresentation attribute(String name, String value) {
-        if (this.attributes == null) attributes = new HashMap<String, String>();
-        attributes.put(name, value);
+    public UserRepresentation singleAttribute(String name, String value) {
+        if (this.attributes == null) attributes = new HashMap<>();
+        attributes.put(name, Arrays.asList(value));
         return this;
     }
 
diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
index c43b58b..6c39849 100644
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java
@@ -110,7 +110,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
         // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now
 
         String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm();
-        return kerberosPrincipal.equals(local.getAttribute(KERBEROS_PRINCIPAL));
+        return kerberosPrincipal.equals(local.getFirstAttribute(KERBEROS_PRINCIPAL));
     }
 
     @Override
@@ -229,7 +229,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
                     return proxied;
                 } else {
                     logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getDisplayName() +
-                            "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getAttribute(KERBEROS_PRINCIPAL));
+                            "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getFirstAttribute(KERBEROS_PRINCIPAL));
                     logger.warn("Will re-create user");
                     session.userStorage().removeUser(realm, user);
                 }
@@ -249,7 +249,7 @@ public class KerberosFederationProvider implements UserFederationProvider {
         user.setEnabled(true);
         user.setEmail(email);
         user.setFederationLink(model.getId());
-        user.setAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());
+        user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm());
 
         if (kerberosConfig.isUpdateProfileFirstLogin()) {
             user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
index c449484..81f058d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
@@ -2,9 +2,12 @@ package org.keycloak.federation.ldap.idm.model;
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.jboss.logging.Logger;
 
@@ -24,10 +27,10 @@ public class LDAPObject {
     // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
     private final List<String> readOnlyAttributeNames = new LinkedList<>();
 
-    private final Map<String, Object> attributes = new HashMap<>();
+    private final Map<String, Set<String>> attributes = new HashMap<>();
 
     // Copy of "attributes" containing lower-cased keys
-    private final Map<String, Object> lowerCasedAttributes = new HashMap<>();
+    private final Map<String, Set<String>> lowerCasedAttributes = new HashMap<>();
 
 
     public String getUuid() {
@@ -71,32 +74,37 @@ public class LDAPObject {
         this.rdnAttributeName = rdnAttributeName;
     }
 
-    public void setAttribute(String attributeName, Object attributeValue) {
-        attributes.put(attributeName, attributeValue);
-        lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
+    public void setSingleAttribute(String attributeName, String attributeValue) {
+        Set<String> asSet = new LinkedHashSet<>();
+        asSet.add(attributeValue);
+        setAttribute(attributeName, asSet);
     }
 
-    public Object getAttributeCaseInsensitive(String name) {
-        return lowerCasedAttributes.get(name.toLowerCase());
+    public void setAttribute(String attributeName, Set<String> attributeValue) {
+        attributes.put(attributeName, attributeValue);
+        lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
     }
 
-    public String getAttributeAsStringCaseInsensitive(String name) {
-        Object attrValue = lowerCasedAttributes.get(name.toLowerCase());
-        if (attrValue != null && !(attrValue instanceof String)) {
-            logger.warnf("Expected String but attribute '%s' has value '%s' of type '%s' ", name, attrValue, attrValue.getClass().getName());
-
-            if (attrValue instanceof Collection) {
-                Collection<String> attrValues = (Collection<String>) attrValue;
-                attrValue = attrValues.iterator().next();
-                logger.warnf("Returning just first founded value '%s' from the collection", attrValue);
-            }
+    // Case-insensitive
+    public String getAttributeAsString(String name) {
+        Set<String> attrValue = lowerCasedAttributes.get(name.toLowerCase());
+        if (attrValue == null || attrValue.size() == 0) {
+            return null;
+        } else if (attrValue.size() > 1) {
+            logger.warnf("Expected String but attribute '%s' has more values '%s' on object '%s' . Returning just first value", name, attrValue, dn);
         }
 
-        return (String) attrValue;
+        return attrValue.iterator().next();
+    }
+
+    // Case-insensitive. Return null if there is not value of attribute with given name or set with all values otherwise
+    public Set<String> getAttributeAsSet(String name) {
+        Set<String> values = lowerCasedAttributes.get(name.toLowerCase());
+        return (values == null) ? null : new LinkedHashSet<>(values);
     }
 
 
-    public Map<String, Object> getAttributes() {
+    public Map<String, Set<String>> getAttributes() {
         return attributes;
     }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
index 01d85a6..23c6d99 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/IdentityStore.java
@@ -4,7 +4,7 @@ import java.util.List;
 
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 
 /**
  * IdentityStore representation providing minimal SPI
@@ -48,9 +48,9 @@ public interface IdentityStore {
 
     // Identity query
 
-    List<LDAPObject> fetchQueryResults(LDAPIdentityQuery LDAPIdentityQuery);
+    List<LDAPObject> fetchQueryResults(LDAPQuery LDAPQuery);
 
-    int countQueryResults(LDAPIdentityQuery LDAPIdentityQuery);
+    int countQueryResults(LDAPQuery LDAPQuery);
 
 //    // Relationship query
 //
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
index 64087ad..67e92c7 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -28,7 +28,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
 import org.keycloak.federation.ldap.idm.query.internal.BetweenCondition;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
 import org.keycloak.federation.ldap.idm.query.internal.GreaterThanCondition;
 import org.keycloak.federation.ldap.idm.query.internal.InCondition;
@@ -108,7 +108,7 @@ public class LDAPIdentityStore implements IdentityStore {
 
 
     @Override
-    public List<LDAPObject> fetchQueryResults(LDAPIdentityQuery identityQuery) {
+    public List<LDAPObject> fetchQueryResults(LDAPQuery identityQuery) {
         if (identityQuery.getSorting() != null && !identityQuery.getSorting().isEmpty()) {
             throw new ModelException("LDAP Identity Store does not yet support sorted queries.");
         }
@@ -160,7 +160,7 @@ public class LDAPIdentityStore implements IdentityStore {
     }
 
     @Override
-    public int countQueryResults(LDAPIdentityQuery identityQuery) {
+    public int countQueryResults(LDAPQuery identityQuery) {
         int limit = identityQuery.getLimit();
         int offset = identityQuery.getOffset();
 
@@ -247,7 +247,7 @@ public class LDAPIdentityStore implements IdentityStore {
 
     // ************ END CREDENTIALS AND USER SPECIFIC STUFF
 
-    protected StringBuilder createIdentityTypeSearchFilter(final LDAPIdentityQuery identityQuery) {
+    protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
         StringBuilder filter = new StringBuilder();
 
         for (Condition condition : identityQuery.getConditions()) {
@@ -400,18 +400,14 @@ public class LDAPIdentityStore implements IdentityStore {
                     Set<String> attrValues = new LinkedHashSet<>();
                     NamingEnumeration<?> enumm = ldapAttribute.getAll();
                     while (enumm.hasMoreElements()) {
-                        String attrVal = enumm.next().toString();
+                        String attrVal = enumm.next().toString().trim();
                         attrValues.add(attrVal);
                     }
 
                     if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
                         ldapObject.setObjectClasses(attrValues);
                     } else {
-                        if (attrValues.size() == 1) {
-                            ldapObject.setAttribute(ldapAttributeName, attrValues.iterator().next());
-                        } else {
-                            ldapObject.setAttribute(ldapAttributeName, attrValues);
-                        }
+                        ldapObject.setAttribute(ldapAttributeName, attrValues);
 
                         // readOnlyAttrNames are lower-cased
                         if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
@@ -435,30 +431,25 @@ public class LDAPIdentityStore implements IdentityStore {
     protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
         BasicAttributes entryAttributes = new BasicAttributes();
 
-        for (Map.Entry<String, Object> attrEntry : ldapObject.getAttributes().entrySet()) {
+        for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
             String attrName = attrEntry.getKey();
-            Object attrValue = attrEntry.getValue();
+            Set<String> attrValue = attrEntry.getValue();
 
             // ldapObject.getReadOnlyAttributeNames() are lower-cased
             if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
-
-                if (String.class.isInstance(attrValue)) {
-                    if (attrValue.toString().trim().length() == 0) {
-                        attrValue = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
-                    }
-                    entryAttributes.put(attrName, attrValue);
-                } else if (Collection.class.isInstance(attrValue)) {
-                    BasicAttribute attr = new BasicAttribute(attrName);
-                    Collection<String> valueCollection = (Collection<String>) attrValue;
-                    for (String val : valueCollection) {
+                BasicAttribute attr = new BasicAttribute(attrName);
+                if (attrValue == null) {
+                    // Adding empty value as we don't know if attribute is mandatory in LDAP
+                    attr.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+                } else {
+                    for (String val : attrValue) {
+                        if (val == null || val.toString().trim().length() == 0) {
+                            val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
+                        }
                         attr.add(val);
                     }
-                    entryAttributes.put(attr);
-                } else if (attrValue == null || attrValue.toString().trim().length() == 0) {
-                    entryAttributes.put(attrName, LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
-                } else {
-                    throw new ModelException("Unexpected type of value of argument " + attrName + ". Value is " + attrValue);
                 }
+                entryAttributes.put(attr);
             }
         }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
index 8d934f3..18b8a86 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -31,7 +31,7 @@ import javax.naming.ldap.PagedResultsResponseControl;
 
 import org.jboss.logging.Logger;
 import org.keycloak.federation.ldap.LDAPConfig;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 
@@ -165,7 +165,7 @@ public class LDAPOperationManager {
         }
     }
 
-    public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPIdentityQuery identityQuery) throws NamingException {
+    public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
         final List<SearchResult> result = new ArrayList<SearchResult>();
         final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index cd857ff..66665bf 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -6,7 +6,7 @@ import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
 import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
@@ -51,7 +51,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
     protected EditMode editMode;
     protected LDAPProviderKerberosConfig kerberosConfig;
 
-    protected final Set<String> supportedCredentialTypes = new HashSet<String>();
+    protected final Set<String> supportedCredentialTypes = new HashSet<>();
 
     public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSession session, UserFederationProviderModel model, LDAPIdentityStore ldapIdentityStore) {
         this.factory = factory;
@@ -145,8 +145,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
         if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
 
         LDAPObject ldapObject = LDAPUtils.addUserToLDAP(this, realm, user);
-        user.setAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid());
-        user.setAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString());
+        user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid());
+        user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString());
 
         return proxy(realm, user, ldapObject);
     }
@@ -202,7 +202,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
         }
 
         if (attributes.containsKey(FIRST_NAME) || attributes.containsKey(LAST_NAME)) {
-            LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+            LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
             LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
 
             // Mapper should replace parameter with correct LDAP mapped attributes
@@ -229,10 +229,10 @@ public class LDAPFederationProvider implements UserFederationProvider {
         if (ldapUser == null) {
             return null;
         }
-        if (ldapUser.getUuid().equals(local.getAttribute(LDAPConstants.LDAP_ID))) {
+        if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) {
             return ldapUser;
         } else {
-            logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getAttribute(LDAPConstants.LDAP_ID));
+            logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getFirstAttribute(LDAPConstants.LDAP_ID));
             return null;
         }
     }
@@ -271,8 +271,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
 
         String userDN = ldapUser.getDn().toString();
         imported.setFederationLink(model.getId());
-        imported.setAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
-        imported.setAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
+        imported.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid());
+        imported.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, userDN);
 
         logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(),
                 ldapUser.getUuid(), userDN);
@@ -280,7 +280,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
     }
 
     protected LDAPObject queryByEmail(RealmModel realm, String email) {
-        LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+        LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
         LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
 
         // Mapper should replace "email" in parameter name with correct LDAP mapped attribute
@@ -395,7 +395,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
                 importUserFromLDAP(realm, ldapUser);
                 syncResult.increaseAdded();
             } else {
-                if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getAttribute(LDAPConstants.LDAP_ID)))) {
+                if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
 
                     // Update keycloak user
                     Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
@@ -435,7 +435,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
                     return proxy(realm, user, ldapObject);
                 } else {
                     logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s",
-                            username,  model.getDisplayName(), user.getAttribute(LDAPConstants.LDAP_ID));
+                            username,  model.getDisplayName(), user.getFirstAttribute(LDAPConstants.LDAP_ID));
                     logger.warn("Will re-create user");
                     session.userStorage().removeUser(realm, user);
                 }
@@ -448,7 +448,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
     }
 
     public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) {
-        LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
+        LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm);
         LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
 
         String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 98ee688..2b60ff8 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -9,7 +9,7 @@ import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
 import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
@@ -184,7 +184,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
     public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
         logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName());
 
-        LDAPIdentityQuery userQuery = createQuery(sessionFactory, realmId, model);
+        LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
         UserFederationSyncResult syncResult = syncImpl(sessionFactory, userQuery, realmId, model);
 
         // TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
@@ -203,7 +203,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(new QueryParameter(LDAPConstants.MODIFY_TIMESTAMP), lastSync);
         Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
 
-        LDAPIdentityQuery userQuery = createQuery(sessionFactory, realmId, model);
+        LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
         userQuery.where(orCondition);
         UserFederationSyncResult result = syncImpl(sessionFactory, userQuery, realmId, model);
 
@@ -211,7 +211,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         return result;
     }
 
-    protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPIdentityQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
+    protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
 
         final UserFederationSyncResult syncResult = new UserFederationSyncResult();
 
@@ -254,9 +254,9 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         return syncResult;
     }
 
-    private LDAPIdentityQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
+    private LDAPQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
         class QueryHolder {
-            LDAPIdentityQuery query;
+            LDAPQuery query;
         }
 
         final QueryHolder queryHolder = new QueryHolder();
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
index 27e8df3..c731330 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
@@ -4,7 +4,7 @@ import java.util.Set;
 
 import org.keycloak.federation.ldap.idm.model.LDAPDn;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
 import org.keycloak.models.ModelException;
@@ -44,8 +44,8 @@ public class LDAPUtils {
         return ldapUser;
     }
 
-    public static LDAPIdentityQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) {
-        LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider);
+    public static LDAPQuery createQueryForUserSearch(LDAPFederationProvider ldapProvider, RealmModel realm) {
+        LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
         LDAPConfig config = ldapProvider.getLdapIdentityStore().getConfig();
         ldapQuery.setSearchScope(config.getSearchScope());
         ldapQuery.setSearchDn(config.getUsersDn());
@@ -60,7 +60,7 @@ public class LDAPUtils {
     // ldapUser has filled attributes, but doesn't have filled dn.
     private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) {
         String rdnLdapAttrName = config.getRdnLdapAttribute();
-        String rdnLdapAttrValue = ldapUser.getAttributeAsStringCaseInsensitive(rdnLdapAttrName);
+        String rdnLdapAttrValue = ldapUser.getAttributeAsString(rdnLdapAttrName);
         if (rdnLdapAttrValue == null) {
             throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes());
         }
@@ -72,6 +72,6 @@ public class LDAPUtils {
 
     public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
         String usernameAttr = config.getUsernameLdapAttribute();
-        return ldapUser.getAttributeAsStringCaseInsensitive(usernameAttr);
+        return ldapUser.getAttributeAsString(usernameAttr);
     }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
index 483cc06..a29581d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
@@ -8,7 +8,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
 import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -28,9 +28,13 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     @Override
     public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
-        String fullName = ldapUser.getAttributeAsStringCaseInsensitive(ldapFullNameAttrName);
+        String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
+        if (fullName == null) {
+            return;
+        }
+
         fullName = fullName.trim();
-        if (fullName != null && !fullName.trim().isEmpty()) {
+        if (!fullName.isEmpty()) {
             int lastSpaceIndex = fullName.lastIndexOf(" ");
             if (lastSpaceIndex == -1) {
                 user.setLastName(fullName);
@@ -45,7 +49,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
         String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
-        ldapUser.setAttribute(ldapFullNameAttrName, fullName);
+        ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
 
         if (isReadOnly(mapperModel)) {
             ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
@@ -80,7 +84,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
                     ensureTransactionStarted();
 
                     String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
-                    ldapUser.setAttribute(ldapFullNameAttrName, fullName);
+                    ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
                 }
 
             };
@@ -92,7 +96,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     }
 
     @Override
-    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
         query.addReturningLdapAttribute(ldapFullNameAttrName);
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
index 4676a6b..8d5ab4e 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
@@ -2,7 +2,7 @@ package org.keycloak.federation.ldap.mappers;
 
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.models.RealmModel;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.UserFederationMapperModel;
@@ -58,5 +58,5 @@ public interface LDAPFederationMapper extends UserFederationMapper {
      * @param mapperModel
      * @param query
      */
-    void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query);
+    void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
index bc9fb05..4565f88 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -12,7 +12,7 @@ import org.keycloak.federation.ldap.idm.model.LDAPDn;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.LDAPConstants;
@@ -58,7 +58,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
     // List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance
     // TODO: Rather address this with caching at LDAPIdentityStore level?
-    private Set<String> rolesSyncedModels = new TreeSet<String>();
+    private Set<String> rolesSyncedModels = new TreeSet<>();
 
     @Override
     public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
@@ -74,7 +74,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             // Import role mappings from LDAP into Keycloak DB
             String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject ldapRole : ldapRoles) {
-                String roleName = ldapRole.getAttributeAsStringCaseInsensitive(roleNameAttr);
+                String roleName = ldapRole.getAttributeAsString(roleNameAttr);
 
                 RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
                 RoleModel role = roleContainer.getRole(roleName);
@@ -95,7 +95,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         if (!rolesSyncedModels.contains(mapperModel.getId())) {
             logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
 
-            LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+            LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
 
             // Send query
             List<LDAPObject> ldapRoles = ldapQuery.getResultList();
@@ -103,7 +103,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
             String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject ldapRole : ldapRoles) {
-                String roleName = ldapRole.getAttributeAsStringCaseInsensitive(rolesRdnAttr);
+                String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
 
                 if (roleContainer.getRole(roleName) == null) {
                     logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName);
@@ -115,8 +115,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         }
     }
 
-    public LDAPIdentityQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
-        LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider);
+    public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
+        LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
 
         // For now, use same search scope, which is configured "globally" and used for user's search.
         ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
@@ -178,7 +178,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         }
         String[] objClasses = objectClasses.split(",");
 
-        Set<String> trimmed = new HashSet<String>();
+        Set<String> trimmed = new HashSet<>();
         for (String objectClass : objClasses) {
             objectClass = objectClass.trim();
             if (objectClass.length() > 0) {
@@ -202,7 +202,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
         ldapObject.setRdnAttributeName(roleNameAttribute);
         ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider));
-        ldapObject.setAttribute(roleNameAttribute, roleName);
+        ldapObject.setSingleAttribute(roleNameAttribute, roleName);
 
         LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
         roleDn.addFirst(roleNameAttribute, roleName);
@@ -220,6 +220,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         }
 
         Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
+
+        // Remove membership placeholder if present
+        for (String membership : memberships) {
+            if (membership.trim().length() == 0) {
+                memberships.remove(membership);
+                break;
+            }
+        }
+
         memberships.add(ldapUser.getDn().toString());
         ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
 
@@ -240,7 +249,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
     }
 
     public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) {
-        LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+        LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
         Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), roleName);
         ldapQuery.where(roleNameCondition);
         return ldapQuery.getFirstResult();
@@ -248,29 +257,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
     protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
         String memberAttrName = getMembershipLdapAttribute(mapperModel);
-        Set<String> memberships = new TreeSet<String>();
-        Object existingMemberships = ldapRole.getAttributeCaseInsensitive(memberAttrName);
-
-        if (existingMemberships != null) {
-            if (existingMemberships instanceof String) {
-                String existingMembership = existingMemberships.toString().trim();
-                if (existingMemberships != null && existingMembership.length() > 0) {
-                    memberships.add(existingMembership);
-                }
-            } else if (existingMemberships instanceof Collection) {
-                Collection<String> exMemberships = (Collection<String>) existingMemberships;
-                for (String membership : exMemberships) {
-                    if (membership.trim().length() > 0) {
-                        memberships.add(membership);
-                    }
-                }
-            }
+        Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
+        if (memberships == null) {
+            memberships = new HashSet<>();
         }
         return memberships;
     }
 
     protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
-        LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+        LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
         String membershipAttr = getMembershipLdapAttribute(mapperModel);
         Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString());
         ldapQuery.where(membershipCondition);
@@ -290,7 +285,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
     }
 
     @Override
-    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
     }
 
 
@@ -389,7 +384,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
             if (mode == Mode.LDAP_ONLY) {
                 // For LDAP-only we want to retrieve role mappings of target container just from LDAP
-                Set<RoleModel> modelRolesCopy = new HashSet<RoleModel>(modelRoleMappings);
+                Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
                 for (RoleModel role : modelRolesCopy) {
                     if (role.getContainer().equals(targetRoleContainer)) {
                         modelRoleMappings.remove(role);
@@ -408,10 +403,10 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
             List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
 
-            Set<RoleModel> roles = new HashSet<RoleModel>();
+            Set<RoleModel> roles = new HashSet<>();
             String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject role : ldapRoles) {
-                String roleName = role.getAttributeAsStringCaseInsensitive(roleNameLdapAttr);
+                String roleName = role.getAttributeAsString(roleNameLdapAttr);
                 RoleModel modelRole = roleContainer.getRole(roleName);
                 if (modelRole == null) {
                     // Add role to local DB
@@ -430,7 +425,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
             if (role.getContainer().equals(roleContainer)) {
 
-                LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+                LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
                 LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
                 Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName());
                 Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString());
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
index c372769..24bf450 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
@@ -1,14 +1,20 @@
 package org.keycloak.federation.ldap.mappers;
 
 import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
+import org.jboss.logging.Logger;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
@@ -23,10 +29,12 @@ import org.keycloak.models.utils.reflection.PropertyQueries;
  */
 public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
+    private static final Logger logger = Logger.getLogger(UserAttributeLDAPFederationMapper.class);
+
     private static final Map<String, Property<Object>> userModelProperties;
 
     static {
-        userModelProperties = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
+        Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
 
             @Override
             public boolean methodMatches(Method m) {
@@ -38,6 +46,12 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
             }
 
         }).getResultList();
+
+        // Convert to be keyed by lower-cased attribute names
+        userModelProperties = new HashMap<>();
+        for (Map.Entry<String, Property<Object>> entry : userModelProps.entrySet()) {
+            userModelProperties.put(entry.getKey().toLowerCase(), entry.getValue());
+        }
     }
 
     public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
@@ -51,16 +65,21 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
-        Object ldapAttrValue = ldapUser.getAttributeCaseInsensitive(ldapAttrName);
-        if (ldapAttrValue != null && !ldapAttrValue.toString().trim().isEmpty()) {
-            Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
+        Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
 
-            if (userModelProperty != null) {
-                // we have java property on UserModel
-                userModelProperty.setValue(user, ldapAttrValue);
+        if (userModelProperty != null) {
+
+            // we have java property on UserModel
+            String ldapAttrValue = ldapUser.getAttributeAsString(ldapAttrName);
+            setPropertyOnUserModel(userModelProperty, user, ldapAttrValue);
+        } else {
+
+            // we don't have java property. Let's set attribute
+            Set<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
+            if (ldapAttrValue != null) {
+                user.setAttribute(userModelAttrName, new ArrayList<>(ldapAttrValue));
             } else {
-                // we don't have java property. Let's just setAttribute
-                user.setAttribute(userModelAttrName, (String) ldapAttrValue);
+                user.removeAttribute(userModelAttrName);
             }
         }
     }
@@ -70,18 +89,26 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
-        Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
+        Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
 
-        Object attrValue;
         if (userModelProperty != null) {
-            // we have java property on UserModel
-            attrValue = userModelProperty.getValue(localUser);
+
+            // we have java property on UserModel. Assuming we support just properties of simple types
+            Object attrValue = userModelProperty.getValue(localUser);
+            String valueAsString = (attrValue == null) ? null : attrValue.toString();
+            ldapUser.setSingleAttribute(ldapAttrName, valueAsString);
         } else {
-            // we don't have java property. Let's just setAttribute
-            attrValue = localUser.getAttribute(userModelAttrName);
+
+            // we don't have java property. Let's set attribute
+            List<String> attrValues = localUser.getAttribute(userModelAttrName);
+
+            if (attrValues.size() == 0) {
+                ldapUser.setAttribute(ldapAttrName, null);
+            } else {
+                ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(attrValues));
+            }
         }
 
-        ldapUser.setAttribute(ldapAttrName, attrValue);
         if (isReadOnly(mapperModel)) {
             ldapUser.addReadOnlyAttributeName(ldapAttrName);
         }
@@ -99,9 +126,21 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
             delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
 
                 @Override
-                public void setAttribute(String name, String value) {
+                public void setSingleAttribute(String name, String value) {
                     setLDAPAttribute(name, value);
-                    super.setAttribute(name, value);
+                    super.setSingleAttribute(name, value);
+                }
+
+                @Override
+                public void setAttribute(String name, List<String> values) {
+                    setLDAPAttribute(name, values);
+                    super.setAttribute(name, values);
+                }
+
+                @Override
+                public void removeAttribute(String name) {
+                    setLDAPAttribute(name, null);
+                    super.removeAttribute(name);
                 }
 
                 @Override
@@ -122,15 +161,22 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
                     super.setFirstName(firstName);
                 }
 
-                protected void setLDAPAttribute(String modelAttrName, String value) {
+                protected void setLDAPAttribute(String modelAttrName, Object value) {
                     if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
                         if (logger.isTraceEnabled()) {
-                            logger.tracef("Pushing user attribute to LDAP. Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", modelAttrName, ldapAttrName, value);
+                            logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
                         }
 
                         ensureTransactionStarted();
 
-                        ldapUser.setAttribute(ldapAttrName, value);
+                        if (value == null) {
+                            ldapUser.setAttribute(ldapAttrName, null);
+                        } else if (value instanceof String) {
+                            ldapUser.setSingleAttribute(ldapAttrName, (String) value);
+                        } else {
+                            List<String> asList = (List<String>) value;
+                            ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
+                        }
                     }
                 }
 
@@ -144,32 +190,48 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
             delegate = new UserModelDelegate(delegate) {
 
                 @Override
-                public String getAttribute(String name) {
+                public String getFirstAttribute(String name) {
+                    if (name.equalsIgnoreCase(userModelAttrName)) {
+                        return ldapUser.getAttributeAsString(ldapAttrName);
+                    } else {
+                        return super.getFirstAttribute(name);
+                    }
+                }
+
+                @Override
+                public List<String> getAttribute(String name) {
                     if (name.equalsIgnoreCase(userModelAttrName)) {
-                        // TODO: Support different types than strings as well...
-                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                        Collection<String> ldapAttrValue = ldapUser.getAttributeAsSet(ldapAttrName);
+                        if (ldapAttrValue == null) {
+                            return null;
+                        } else {
+                            return new ArrayList<>(ldapAttrValue);
+                        }
                     } else {
                         return super.getAttribute(name);
                     }
                 }
 
                 @Override
-                public Map<String, String> getAttributes() {
-                    Map<String, String> attrs = new HashMap<>(super.getAttributes());
+                public Map<String, List<String>> getAttributes() {
+                    Map<String, List<String>> attrs = new HashMap<>(super.getAttributes());
 
-                    // Ignore properties
-                    if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName) || UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName) || UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
+                    // Ignore UserModel properties
+                    if (userModelProperties.get(userModelAttrName.toLowerCase()) != null) {
                         return attrs;
                     }
 
-                    attrs.put(userModelAttrName, ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName));
+                    Set<String> allLdapAttrValues = ldapUser.getAttributeAsSet(ldapAttrName);
+                    if (allLdapAttrValues != null) {
+                        attrs.put(userModelAttrName, new ArrayList<>(allLdapAttrValues));
+                    }
                     return attrs;
                 }
 
                 @Override
                 public String getEmail() {
                     if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
-                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                        return ldapUser.getAttributeAsString(ldapAttrName);
                     } else {
                         return super.getEmail();
                     }
@@ -178,7 +240,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
                 @Override
                 public String getLastName() {
                     if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
-                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                        return ldapUser.getAttributeAsString(ldapAttrName);
                     } else {
                         return super.getLastName();
                     }
@@ -187,7 +249,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
                 @Override
                 public String getFirstName() {
                     if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) {
-                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                        return ldapUser.getAttributeAsString(ldapAttrName);
                     } else {
                         return super.getFirstName();
                     }
@@ -200,7 +262,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     }
 
     @Override
-    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
@@ -222,4 +284,22 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     private boolean isReadOnly(UserFederationMapperModel mapperModel) {
         return parseBooleanParameter(mapperModel, READ_ONLY);
     }
+
+
+    protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserModel user, String ldapAttrValue) {
+        if (ldapAttrValue == null) {
+            userModelProperty.setValue(user, null);
+        } else {
+            Class<Object> clazz = userModelProperty.getJavaClass();
+
+            if (String.class.equals(clazz)) {
+                userModelProperty.setValue(user, ldapAttrValue);
+            } else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
+                Boolean boolVal = Boolean.valueOf(ldapAttrValue);
+                userModelProperty.setValue(user, boolVal);
+            } else {
+                logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
+            }
+        }
+    }
 }
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
index 2032370..c152e11 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
@@ -1,9 +1,12 @@
 package org.keycloak.account.freemarker.model;
 
+import org.jboss.logging.Logger;
 import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
 
 import javax.ws.rs.core.MultivaluedMap;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -11,14 +14,29 @@ import java.util.Map;
  */
 public class AccountBean {
 
+    private static final Logger logger = Logger.getLogger(AccountBean.class);
+
     private final UserModel user;
     private final MultivaluedMap<String, String> profileFormData;
+
+    // TODO: More proper multi-value attribute support
     private final Map<String, String> attributes = new HashMap<>();
 
     public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
         this.user = user;
         this.profileFormData = profileFormData;
-        attributes.putAll(user.getAttributes());
+
+        for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
+            List<String> attrValue = attr.getValue();
+            if (attrValue.size() > 0) {
+                attributes.put(attr.getKey(), attrValue.get(0));
+            }
+
+            if (attrValue.size() > 1) {
+                logger.warnf("There are more values for attribute '%s' of user '%s' . Will display just first value", attr.getKey(), user.getUsername());
+            }
+        }
+
         if (profileFormData != null) {
             for (String key : profileFormData.keySet()) {
                 if (key.startsWith("user.attributes.")) {
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
index 0029dfd..fc249d5 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java
@@ -35,7 +35,7 @@ public class LocaleHelper {
             Locale locale =  findLocale(realm.getSupportedLocales(), localeString);
             if(locale != null){
                 if(user != null){
-                    user.setAttribute(UserModel.LOCALE, locale.toLanguageTag());
+                    user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
                 }
                 return locale;
             }else{
@@ -48,8 +48,8 @@ public class LocaleHelper {
             String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
             Locale locale =  findLocale(realm.getSupportedLocales(), localeString);
             if(locale != null){
-                if(user != null && user.getAttribute(UserModel.LOCALE) == null){
-                    user.setAttribute(UserModel.LOCALE, locale.toLanguageTag());
+                if(user != null && user.getFirstAttribute(UserModel.LOCALE) == null){
+                    user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
                 }
                 return locale;
             }else{
@@ -59,7 +59,7 @@ public class LocaleHelper {
 
         //2. User profile
         if(user != null && user.getAttributes().containsKey(UserModel.LOCALE)){
-            String localeString = user.getAttribute(UserModel.LOCALE);
+            String localeString = user.getFirstAttribute(UserModel.LOCALE);
             Locale locale =  findLocale(realm.getSupportedLocales(), localeString);
             if(locale != null){
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 2ff1273..f405994 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -206,6 +206,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
         if (!user.attributes) {
             user.attributes = {}
         }
+        convertAttributeValuesToString(user);
+
         $scope.user = angular.copy(user);
         if(user.federationLink) {
             console.log("federationLink is not null");
@@ -252,13 +254,15 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
     }, true);
 
     $scope.save = function() {
+        convertAttributeValuesToLists();
+
         if ($scope.create) {
             User.save({
                 realm: realm.realm
             }, $scope.user, function (data, headers) {
                 $scope.changed = false;
+                convertAttributeValuesToString($scope.user);
                 user = angular.copy($scope.user);
-
                 var l = headers().location;
 
                 console.debug("Location == " + l);
@@ -275,12 +279,33 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede
                 userId: $scope.user.id
             }, $scope.user, function () {
                 $scope.changed = false;
+                convertAttributeValuesToString($scope.user);
                 user = angular.copy($scope.user);
                 Notifications.success("Your changes have been saved to the user.");
             });
         }
     };
 
+    function convertAttributeValuesToLists() {
+        var attrs = $scope.user.attributes;
+        for (var attribute in attrs) {
+            if (typeof attrs[attribute] === "string") {
+                var attrVals = attrs[attribute].split("##");
+                attrs[attribute] = attrVals;
+            }
+        }
+    }
+
+    function convertAttributeValuesToString(user) {
+        var attrs = user.attributes;
+        for (var attribute in attrs) {
+            if (typeof attrs[attribute] === "object") {
+                var attrVals = attrs[attribute].join("##");
+                attrs[attribute] = attrVals;
+            }
+        }
+    }
+
     $scope.reset = function() {
         $scope.user = angular.copy(user);
         $scope.changed = false;
diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index 064697b..d21d203 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
@@ -1,7 +1,5 @@
 package org.keycloak.models.entities;
 
-import org.keycloak.models.UserModel;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -23,7 +21,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
 
     private List<String> roleIds;
 
-    private Map<String, String> attributes;
+    private Map<String, List<String>> attributes;
     private List<String> requiredActions;
     private List<CredentialEntity> credentials = new ArrayList<CredentialEntity>();
     private List<FederatedIdentityEntity> federatedIdentities;
@@ -101,11 +99,11 @@ public class UserEntity extends AbstractIdentifiableEntity {
         this.roleIds = roleIds;
     }
 
-    public Map<String, String> getAttributes() {
+    public Map<String, List<String>> getAttributes() {
         return attributes;
     }
 
-    public void setAttributes(Map<String, String> attributes) {
+    public void setAttributes(Map<String, List<String>> attributes) {
         this.attributes = attributes;
     }
 
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 645250e..dea9e7b 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -27,13 +27,31 @@ public interface UserModel {
 
     void setEnabled(boolean enabled);
 
-    void setAttribute(String name, String value);
+    /**
+     * Set single value of specified attribute. Remove all other existing values
+     *
+     * @param name
+     * @param value
+     */
+    void setSingleAttribute(String name, String value);
+
+    void setAttribute(String name, List<String> values);
 
     void removeAttribute(String name);
 
-    String getAttribute(String name);
+    /**
+     * @param name
+     * @return null if there is not any value of specified attribute or first value otherwise. Don't throw exception if there are more values of the attribute
+     */
+    String getFirstAttribute(String name);
+
+    /**
+     * @param name
+     * @return list of all attribute values or empty list if there are not any values. Never return null
+     */
+    List<String> getAttribute(String name);
 
-    Map<String, String> getAttributes();
+    Map<String, List<String>> getAttributes();
 
     Set<String> getRequiredActions();
 
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 467040f..3bf412c 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -31,6 +31,7 @@ import org.keycloak.representations.idm.UserFederationMapperRepresentation;
 import org.keycloak.representations.idm.UserFederationProviderRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.util.MultivaluedHashMap;
 import org.keycloak.util.Time;
 
 import java.util.ArrayList;
@@ -67,7 +68,7 @@ public class ModelToRepresentation {
         rep.setRequiredActions(reqActions);
 
         if (user.getAttributes() != null && !user.getAttributes().isEmpty()) {
-            Map<String, String> attrs = new HashMap<String, String>();
+            Map<String, Object> attrs = new HashMap<>();
             attrs.putAll(user.getAttributes());
             rep.setAttributes(attrs);
         }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 245e6c6..217da67 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -43,6 +43,7 @@ import org.keycloak.util.UriUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -805,8 +806,17 @@ public class RepresentationToModel {
         user.setFederationLink(userRep.getFederationLink());
         user.setTotp(userRep.isTotp());
         if (userRep.getAttributes() != null) {
-            for (Map.Entry<String, String> entry : userRep.getAttributes().entrySet()) {
-                user.setAttribute(entry.getKey(), entry.getValue());
+            for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
+                Object value = entry.getValue();
+
+                if (value instanceof Collection) {
+                    Collection<String> colVal = (Collection<String>) value;
+                    user.setAttribute(entry.getKey(), new ArrayList<>(colVal));
+                } else if (value instanceof String) {
+                    // TODO: This is here just for backwards compatibility with KC 1.3 and earlier
+                    String stringVal = (String) value;
+                    user.setSingleAttribute(entry.getKey(), stringVal);
+                }
             }
         }
         if (userRep.getRequiredActions() != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 7123c3e..699a38e 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -53,8 +53,13 @@ public class UserModelDelegate implements UserModel {
     }
 
     @Override
-    public void setAttribute(String name, String value) {
-        delegate.setAttribute(name, value);
+    public void setSingleAttribute(String name, String value) {
+        delegate.setSingleAttribute(name, value);
+    }
+
+    @Override
+    public void setAttribute(String name, List<String> values) {
+        delegate.setAttribute(name, values);
     }
 
     @Override
@@ -63,12 +68,17 @@ public class UserModelDelegate implements UserModel {
     }
 
     @Override
-    public String getAttribute(String name) {
+    public String getFirstAttribute(String name) {
+        return delegate.getFirstAttribute(name);
+    }
+
+    @Override
+    public List<String> getAttribute(String name) {
         return delegate.getAttribute(name);
     }
 
     @Override
-    public Map<String, String> getAttributes() {
+    public Map<String, List<String>> getAttributes() {
         return delegate.getAttributes();
     }
 
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 39024c1..5c33375 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -158,12 +158,23 @@ public class UserAdapter implements UserModel, Comparable {
     }
 
     @Override
-    public void setAttribute(String name, String value) {
+    public void setSingleAttribute(String name, String value) {
         if (user.getAttributes() == null) {
-            user.setAttributes(new HashMap<String, String>());
+            user.setAttributes(new HashMap<String, List<String>>());
         }
 
-        user.getAttributes().put(name, value);
+        List<String> attrValues = new ArrayList<>();
+        attrValues.add(value);
+        user.getAttributes().put(name, attrValues);
+    }
+
+    @Override
+    public void setAttribute(String name, List<String> values) {
+        if (user.getAttributes() == null) {
+            user.setAttributes(new HashMap<String, List<String>>());
+        }
+
+        user.getAttributes().put(name, values);
     }
 
     @Override
@@ -174,13 +185,23 @@ public class UserAdapter implements UserModel, Comparable {
     }
 
     @Override
-    public String getAttribute(String name) {
-        return user.getAttributes()==null ? null : user.getAttributes().get(name);
+    public String getFirstAttribute(String name) {
+        if (user.getAttributes()==null) return null;
+
+        List<String> attrValues = user.getAttributes().get(name);
+        return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
+    }
+
+    @Override
+    public List<String> getAttribute(String name) {
+        if (user.getAttributes()==null) return Collections.<String>emptyList();
+        List<String> attrValues = user.getAttributes().get(name);
+        return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
     }
 
     @Override
-    public Map<String, String> getAttributes() {
-        return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes());
+    public Map<String, List<String>> getAttributes() {
+        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
     }
 
     @Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index bbef81b..d7824cc 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -4,6 +4,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.util.MultivaluedHashMap;
 
 import java.io.Serializable;
 import java.util.HashMap;
@@ -29,7 +30,7 @@ public class CachedUser implements Serializable {
     private boolean enabled;
     private boolean totp;
     private String federationLink;
-    private Map<String, String> attributes = new HashMap<>();
+    private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
     private Set<String> requiredActions = new HashSet<>();
     private Set<String> roleMappings = new HashSet<String>();
 
@@ -93,7 +94,7 @@ public class CachedUser implements Serializable {
         return totp;
     }
 
-    public Map<String, String> getAttributes() {
+    public MultivaluedHashMap<String, String> getAttributes() {
         return attributes;
     }
 
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
index aa80a25..fb516c6 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java
@@ -11,6 +11,7 @@ import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.cache.entities.CachedUser;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -78,9 +79,15 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public void setAttribute(String name, String value) {
+    public void setSingleAttribute(String name, String value) {
         getDelegateForUpdate();
-        updated.setAttribute(name, value);
+        updated.setSingleAttribute(name, value);
+    }
+
+    @Override
+    public void setAttribute(String name, List<String> values) {
+        getDelegateForUpdate();
+        updated.setAttribute(name, values);
     }
 
     @Override
@@ -90,13 +97,20 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public String getAttribute(String name) {
+    public String getFirstAttribute(String name) {
+        if (updated != null) return updated.getFirstAttribute(name);
+        return cached.getAttributes().getFirst(name);
+    }
+
+    @Override
+    public List<String> getAttribute(String name) {
         if (updated != null) return updated.getAttribute(name);
-        return cached.getAttributes().get(name);
+        List<String> result = cached.getAttributes().get(name);
+        return (result == null) ? Collections.<String>emptyList() : result;
     }
 
     @Override
-    public Map<String, String> getAttributes() {
+    public Map<String, List<String>> getAttributes() {
         if (updated != null) return updated.getAttributes();
         return cached.getAttributes();
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
index 2454f96..51352b4 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
@@ -1,6 +1,8 @@
 package org.keycloak.models.jpa.entities;
 
+import javax.persistence.CollectionTable;
 import javax.persistence.Column;
+import javax.persistence.ElementCollection;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
 import javax.persistence.Id;
@@ -11,6 +13,8 @@ import javax.persistence.NamedQueries;
 import javax.persistence.NamedQuery;
 import javax.persistence.Table;
 import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -22,20 +26,29 @@ import java.io.Serializable;
 })
 @Table(name="USER_ATTRIBUTE")
 @Entity
-@IdClass(UserAttributeEntity.Key.class)
 public class UserAttributeEntity {
 
     @Id
+    @Column(name="ID", length = 36)
+    protected String id;
+
     @ManyToOne(fetch= FetchType.LAZY)
     @JoinColumn(name = "USER_ID")
     protected UserEntity user;
 
-    @Id
     @Column(name = "NAME")
     protected String name;
     @Column(name = "VALUE")
     protected String value;
 
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
     public String getName() {
         return name;
     }
@@ -60,47 +73,4 @@ public class UserAttributeEntity {
         this.user = user;
     }
 
-    public static class Key implements Serializable {
-
-        protected UserEntity user;
-
-        protected String name;
-
-        public Key() {
-        }
-
-        public Key(UserEntity user, String name) {
-            this.user = user;
-            this.name = name;
-        }
-
-        public UserEntity getUser() {
-            return user;
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-
-            Key key = (Key) o;
-
-            if (name != null ? !name.equals(key.name) : key.name != null) return false;
-            if (user != null ? !user.getId().equals(key.user != null ? key.user.getId() : null) : key.user != null) return false;
-
-            return true;
-        }
-
-        @Override
-        public int hashCode() {
-            int result = user != null ? user.getId().hashCode() : 0;
-            result = 31 * result + (name != null ? name.hashCode() : 0);
-            return result;
-        }
-    }
-
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 670f5f0..3719b9d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -22,6 +22,7 @@ import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
 import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
+import org.keycloak.util.MultivaluedHashMap;
 import org.keycloak.util.Time;
 
 import javax.persistence.EntityManager;
@@ -92,14 +93,46 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public void setAttribute(String name, String value) {
+    public void setSingleAttribute(String name, String value) {
+        boolean found = false;
+        List<UserAttributeEntity> toRemove = new ArrayList<>();
         for (UserAttributeEntity attr : user.getAttributes()) {
             if (attr.getName().equals(name)) {
-                attr.setValue(value);
-                return;
+                if (!found) {
+                    attr.setValue(value);
+                    found = true;
+                } else {
+                    toRemove.add(attr);
+                }
             }
         }
+
+        for (UserAttributeEntity attr : toRemove) {
+            em.remove(attr);
+            user.getAttributes().remove(attr);
+        }
+
+        if (found) {
+            return;
+        }
+
+        persistAttributeValue(name, value);
+    }
+
+    @Override
+    public void setAttribute(String name, List<String> values) {
+        // Remove all existing
+        removeAttribute(name);
+
+        // Put all new
+        for (String value : values) {
+            persistAttributeValue(name, value);
+        }
+    }
+
+    private void persistAttributeValue(String name, String value) {
         UserAttributeEntity attr = new UserAttributeEntity();
+        attr.setId(KeycloakModelUtils.generateId());
         attr.setName(name);
         attr.setValue(value);
         attr.setUser(user);
@@ -120,7 +153,7 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public String getAttribute(String name) {
+    public String getFirstAttribute(String name) {
         for (UserAttributeEntity attr : user.getAttributes()) {
             if (attr.getName().equals(name)) {
                 return attr.getValue();
@@ -130,10 +163,21 @@ public class UserAdapter implements UserModel {
     }
 
     @Override
-    public Map<String, String> getAttributes() {
-        Map<String, String> result = new HashMap<String, String>();
+    public List<String> getAttribute(String name) {
+        List<String> result = new ArrayList<>();
+        for (UserAttributeEntity attr : user.getAttributes()) {
+            if (attr.getName().equals(name)) {
+                result.add(attr.getValue());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public Map<String, List<String>> getAttributes() {
+        MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
         for (UserAttributeEntity attr : user.getAttributes()) {
-            result.put(attr.getName(), attr.getValue());
+            result.add(attr.getName(), attr.getValue());
         }
         return result;
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 79a6260..dc858ef 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -19,7 +19,6 @@ import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.entities.CredentialEntity;
 import org.keycloak.models.entities.UserConsentEntity;
-import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
 import org.keycloak.models.mongo.utils.MongoModelUtils;
@@ -127,12 +126,24 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     }
 
     @Override
-    public void setAttribute(String name, String value) {
+    public void setSingleAttribute(String name, String value) {
         if (user.getAttributes() == null) {
-            user.setAttributes(new HashMap<String, String>());
+            user.setAttributes(new HashMap<String, List<String>>());
         }
 
-        user.getAttributes().put(name, value);
+        List<String> attrValues = new ArrayList<>();
+        attrValues.add(value);
+        user.getAttributes().put(name, attrValues);
+        updateUser();
+    }
+
+    @Override
+    public void setAttribute(String name, List<String> values) {
+        if (user.getAttributes() == null) {
+            user.setAttributes(new HashMap<String, List<String>>());
+        }
+
+        user.getAttributes().put(name, values);
         updateUser();
     }
 
@@ -145,13 +156,23 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     }
 
     @Override
-    public String getAttribute(String name) {
-        return user.getAttributes()==null ? null : user.getAttributes().get(name);
+    public String getFirstAttribute(String name) {
+        if (user.getAttributes()==null) return null;
+
+        List<String> attrValues = user.getAttributes().get(name);
+        return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
+    }
+
+    @Override
+    public List<String> getAttribute(String name) {
+        if (user.getAttributes()==null) return Collections.<String>emptyList();
+        List<String> attrValues = user.getAttributes().get(name);
+        return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
     }
 
     @Override
-    public Map<String, String> getAttributes() {
-        return user.getAttributes()==null ? Collections.<String, String>emptyMap() : Collections.unmodifiableMap(user.getAttributes());
+    public Map<String, List<String>> getAttributes() {
+        return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes());
     }
 
     public MongoUserEntity getUser() {
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
index 671abb2..b5f59ad 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
@@ -13,7 +13,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * Mappings UserModel property (the property name of a getter method) to an AttributeStatement.
+ * Mappings UserModel attribute (not property name of a getter method) to an AttributeStatement.
  *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -62,7 +62,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp
     public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
         UserModel user = userSession.getUser();
         String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
-        String attributeValue = user.getAttribute(attributeName);
+        String attributeValue = user.getFirstAttribute(attributeName);
         if (attributeValue == null) return;
         AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue);
 
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index d5630bd..6dcef81 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -238,11 +238,11 @@ public class SamlProtocol implements LoginProtocol {
             // generate a persistent user id specifically for each client.
             UserModel user = userSession.getUser();
             String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId();
-            String samlPersistentId = user.getAttribute(name);
+            String samlPersistentId = user.getFirstAttribute(name);
             if (samlPersistentId != null) return samlPersistentId;
             // "G-" stands for "generated"
             samlPersistentId = "G-" + UUID.randomUUID().toString();
-            user.setAttribute(name, samlPersistentId);
+            user.setSingleAttribute(name, samlPersistentId);
             return samlPersistentId;
         } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){
             // TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
index 5ebff0d..64fc4cd 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AddressMapper.java
@@ -118,11 +118,11 @@ public class AddressMapper extends AbstractOIDCProtocolMapper implements OIDCAcc
     protected void setClaim(IDToken token, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
         AddressClaimSet addressSet = new AddressClaimSet();
-        addressSet.setStreetAddress(user.getAttribute("street"));
-        addressSet.setLocality(user.getAttribute("locality"));
-        addressSet.setRegion(user.getAttribute("region"));
-        addressSet.setPostalCode(user.getAttribute("postal_code"));
-        addressSet.setCountry(user.getAttribute("country"));
+        addressSet.setStreetAddress(user.getFirstAttribute("street"));
+        addressSet.setLocality(user.getFirstAttribute("locality"));
+        addressSet.setRegion(user.getFirstAttribute("region"));
+        addressSet.setPostalCode(user.getFirstAttribute("postal_code"));
+        addressSet.setCountry(user.getFirstAttribute("country"));
         token.getOtherClaims().put("address", addressSet);
     }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
index b854351..164f7a5 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
@@ -3,7 +3,6 @@ package org.keycloak.protocol.oidc.mappers;
 import org.keycloak.models.ClientSessionModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
@@ -77,7 +76,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
     protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
         String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
-        String attributeValue = user.getAttribute(attributeName);
+        String attributeValue = user.getFirstAttribute(attributeName);
         if (attributeValue == null) return;
         OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index b27e453..b6f1fb1 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -5,7 +5,6 @@ import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.BadRequestException;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.keycloak.ClientConnection;
-import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailProvider;
@@ -228,8 +227,8 @@ public class UsersResource {
             }
         }
 
-        if (rep.getAttributes() != null) {
-            for (Map.Entry<String, String> attr : rep.getAttributes().entrySet()) {
+        if (rep.getAttributesAsListValues() != null) {
+            for (Map.Entry<String, List<String>> attr : rep.getAttributesAsListValues().entrySet()) {
                 user.setAttribute(attr.getKey(), attr.getValue());
             }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
index e426b10..10ce8db 100755
--- a/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
+++ b/services/src/main/java/org/keycloak/services/resources/AttributeFormDataProcessor.java
@@ -1,5 +1,8 @@
 package org.keycloak.services.resources;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 
@@ -21,8 +24,26 @@ public class AttributeFormDataProcessor {
         for (String key : formData.keySet()) {
             if (!key.startsWith("user.attributes.")) continue;
             String attribute = key.substring("user.attributes.".length());
-            user.setAttribute(attribute, formData.getFirst(key));
+
+            // Need to handle case when attribute has multiple values, but in UI was displayed just first value
+            List<String> modelValue = new ArrayList<>(user.getAttribute(attribute));
+
+            int index = 0;
+            for (String value : formData.get(key)) {
+                addOrSetValue(modelValue, index, value);
+                index++;
+            }
+
+            user.setAttribute(attribute, modelValue);
         }
 
     }
+
+    private static void addOrSetValue(List<String> list, int index, String value) {
+        if (list.size() > index) {
+            list.set(index, value);
+        } else {
+            list.add(value);
+        }
+    }
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
index 526813b..dd572d7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java
@@ -6,6 +6,7 @@ import org.apache.http.HttpResponse;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.impl.client.DefaultHttpClient;
+import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.ClassRule;
 import org.junit.Rule;
@@ -51,8 +52,8 @@ public class ProfileTest {
             UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
             user.setFirstName("First");
             user.setLastName("Last");
-            user.setAttribute("key1", "value1");
-            user.setAttribute("key2", "value2");
+            user.setSingleAttribute("key1", "value1");
+            user.setSingleAttribute("key2", "value2");
 
             ClientModel accountApp = appRealm.getClientByClientId(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
 
@@ -114,8 +115,12 @@ public class ProfileTest {
         assertEquals("Last", profile.getString("lastName"));
 
         JSONObject attributes = profile.getJSONObject("attributes");
-        assertEquals("value1", attributes.getString("key1"));
-        assertEquals("value2", attributes.getString("key2"));
+        JSONArray attrValue = attributes.getJSONArray("key1");
+        assertEquals(1, attrValue.length());
+        assertEquals("value1", attrValue.get(0));
+        attrValue = attributes.getJSONArray("key2");
+        assertEquals(1, attrValue.length());
+        assertEquals("value2", attrValue.get(0));
     }
 
     @Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 42dd464..ab644cd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -12,6 +12,8 @@ import org.keycloak.representations.idm.UserRepresentation;
 
 import javax.ws.rs.ClientErrorException;
 import javax.ws.rs.core.Response;
+
+import java.util.ArrayList;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
@@ -271,8 +273,8 @@ public class UserTest extends AbstractClientTest {
     public void attributes() {
         UserRepresentation user1 = new UserRepresentation();
         user1.setUsername("user1");
-        user1.attribute("attr1", "value1user1");
-        user1.attribute("attr2", "value2user1");
+        user1.singleAttribute("attr1", "value1user1");
+        user1.singleAttribute("attr2", "value2user1");
 
         Response response = realm.users().create(user1);
         String user1Id = ApiUtil.getCreatedId(response);
@@ -280,40 +282,45 @@ public class UserTest extends AbstractClientTest {
 
         UserRepresentation user2 = new UserRepresentation();
         user2.setUsername("user2");
-        user2.attribute("attr1", "value1user2");
-        user2.attribute("attr2", "value2user2");
+        user2.singleAttribute("attr1", "value1user2");
+        List<String> vals = new ArrayList<>();
+        vals.add("value2user2");
+        vals.add("value2user2_2");
+        user2.getAttributesAsListValues().put("attr2", vals);
 
         response = realm.users().create(user2);
         String user2Id = ApiUtil.getCreatedId(response);
         response.close();
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(2, user1.getAttributes().size());
-        assertEquals("value1user1", user1.getAttributes().get("attr1"));
-        assertEquals("value2user1", user1.getAttributes().get("attr2"));
+        assertEquals(2, user1.getAttributesAsListValues().size());
+        assertAttributeValue("value1user1", user1.getAttributesAsListValues().get("attr1"));
+        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
 
         user2 = realm.users().get(user2Id).toRepresentation();
-        assertEquals(2, user2.getAttributes().size());
-        assertEquals("value1user2", user2.getAttributes().get("attr1"));
-        assertEquals("value2user2", user2.getAttributes().get("attr2"));
+        assertEquals(2, user2.getAttributesAsListValues().size());
+        assertAttributeValue("value1user2", user2.getAttributesAsListValues().get("attr1"));
+        vals = user2.getAttributesAsListValues().get("attr2");
+        assertEquals(2, vals.size());
+        assertTrue(vals.contains("value2user2") && vals.contains("value2user2_2"));
 
-        user1.attribute("attr1", "value3user1");
-        user1.attribute("attr3", "value4user1");
+        user1.singleAttribute("attr1", "value3user1");
+        user1.singleAttribute("attr3", "value4user1");
 
         realm.users().get(user1Id).update(user1);
 
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(3, user1.getAttributes().size());
-        assertEquals("value3user1", user1.getAttributes().get("attr1"));
-        assertEquals("value2user1", user1.getAttributes().get("attr2"));
-        assertEquals("value4user1", user1.getAttributes().get("attr3"));
+        assertEquals(3, user1.getAttributesAsListValues().size());
+        assertAttributeValue("value3user1", user1.getAttributesAsListValues().get("attr1"));
+        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
+        assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
 
         user1.getAttributes().remove("attr1");
         realm.users().get(user1Id).update(user1);
 
         user1 = realm.users().get(user1Id).toRepresentation();
-        assertEquals(2, user1.getAttributes().size());
-        assertEquals("value2user1", user1.getAttributes().get("attr2"));
-        assertEquals("value4user1", user1.getAttributes().get("attr3"));
+        assertEquals(2, user1.getAttributesAsListValues().size());
+        assertAttributeValue("value2user1", user1.getAttributesAsListValues().get("attr2"));
+        assertAttributeValue("value4user1", user1.getAttributesAsListValues().get("attr3"));
 
         user1.getAttributes().clear();
         realm.users().get(user1Id).update(user1);
@@ -322,6 +329,11 @@ public class UserTest extends AbstractClientTest {
         assertNull(user1.getAttributes());
     }
 
+    private void assertAttributeValue(String expectedValue, List<String> attrValues) {
+        assertEquals(1, attrValues.size());
+        assertEquals(expectedValue, attrValues.get(0));
+    }
+
     @Test
     public void sendResetPasswordEmail() {
         UserRepresentation userRep = new UserRepresentation();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 3f4ffa5..ba6d44b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -141,7 +141,7 @@ public abstract class AbstractIdentityProviderTest {
         identityProviderModel.setUpdateProfileFirstLoginMode(IdentityProviderRepresentation.UPFLM_ON);
 
         UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true);
-        Assert.assertEquals("617-666-7777", user.getAttribute("mobile"));
+        Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile"));
     }
 
     @Test
@@ -304,7 +304,7 @@ public abstract class AbstractIdentityProviderTest {
             identityProviderModel.setTrustEmail(true);
 
             UserModel user = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "new@email.com", true);
-            Assert.assertEquals("617-666-7777", user.getAttribute("mobile"));
+            Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile"));
         } finally {
             identityProviderModel.setTrustEmail(false);
             getRealm().setVerifyEmail(false);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
index 1643aa0..c4a028c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
@@ -279,7 +279,7 @@ public class FederationProvidersIntegrationTest {
 
             // Fetch user from LDAP and check that postalCode is filled
             UserModel user = session.users().getUserByUsername("johnzip", appRealm);
-            String postalCode = user.getAttribute("postal_code");
+            String postalCode = user.getFirstAttribute("postal_code");
             Assert.assertEquals("12398", postalCode);
 
         } finally {
@@ -299,21 +299,21 @@ public class FederationProvidersIntegrationTest {
 
             // Fetch user from LDAP and check that postalCode is filled
             UserModel user = session.users().getUserByUsername("johndirect", appRealm);
-            String postalCode = user.getAttribute("postal_code");
+            String postalCode = user.getFirstAttribute("postal_code");
             Assert.assertEquals("12399", postalCode);
 
             // Directly update user in LDAP
-            johnDirect.setAttribute(LDAPConstants.POSTAL_CODE, "12400");
-            johnDirect.setAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
+            johnDirect.setSingleAttribute(LDAPConstants.POSTAL_CODE, "12400");
+            johnDirect.setSingleAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
             ldapFedProvider.getLdapIdentityStore().update(johnDirect);
 
             // Verify that postalCode is still the same as we read it's value from Keycloak DB
             user = session.users().getUserByUsername("johndirect", appRealm);
-            postalCode = user.getAttribute("postal_code");
+            postalCode = user.getFirstAttribute("postal_code");
             Assert.assertEquals("12399", postalCode);
 
             // Check user.getAttributes()
-            postalCode = user.getAttributes().get("postal_code");
+            postalCode = user.getAttributes().get("postal_code").get(0);
             Assert.assertEquals("12399", postalCode);
 
             // LastName is new as lastName mapper will read the value from LDAP
@@ -339,11 +339,11 @@ public class FederationProvidersIntegrationTest {
 
             // Verify that postalCode is read from LDAP now
             UserModel user = session.users().getUserByUsername("johndirect", appRealm);
-            String postalCode = user.getAttribute("postal_code");
+            String postalCode = user.getFirstAttribute("postal_code");
             Assert.assertEquals("12400", postalCode);
 
             // Check user.getAttributes()
-            postalCode = user.getAttributes().get("postal_code");
+            postalCode = user.getAttributes().get("postal_code").get(0);
             Assert.assertEquals("12400", postalCode);
 
             Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index 1a78875..56c4a70 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
@@ -1,5 +1,7 @@
 package org.keycloak.testsuite.federation;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.junit.Assert;
@@ -7,7 +9,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
 import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory;
@@ -69,11 +71,11 @@ class FederationTestUtils {
             }
 
             @Override
-            public String getAttribute(String name) {
+            public List<String> getAttribute(String name) {
                 if ("postal_code".equals(name)) {
-                    return postalCode;
+                    return Arrays.asList(postalCode);
                 } else {
-                    return null;
+                    return Collections.emptyList();
                 }
             }
         };
@@ -91,7 +93,7 @@ class FederationTestUtils {
         Assert.assertEquals(expectedFirstName, user.getFirstName());
         Assert.assertEquals(expectedLastName, user.getLastName());
         Assert.assertEquals(expectedEmail, user.getEmail());
-        Assert.assertEquals(expectedPostalCode, user.getAttribute("postal_code"));
+        Assert.assertEquals(expectedPostalCode, user.getFirstAttribute("postal_code"));
     }
 
     public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
@@ -138,7 +140,7 @@ class FederationTestUtils {
 
     public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
         LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
-        LDAPIdentityQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
+        LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
         List<LDAPObject> allUsers = ldapQuery.getResultList();
 
         for (LDAPObject ldapUser : allUsers) {
@@ -149,7 +151,7 @@ class FederationTestUtils {
     public static void removeAllLDAPRoles(KeycloakSession session, RealmModel appRealm, UserFederationProviderModel ldapModel, String mapperName) {
         UserFederationMapperModel mapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), mapperName);
         LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
-        LDAPIdentityQuery roleQuery = new RoleLDAPFederationMapper().createRoleQuery(mapperModel, ldapProvider);
+        LDAPQuery roleQuery = new RoleLDAPFederationMapper().createRoleQuery(mapperModel, ldapProvider);
         List<LDAPObject> ldapRoles = roleQuery.getResultList();
         for (LDAPObject ldapRole : ldapRoles) {
             ldapProvider.getLdapIdentityStore().remove(ldapRole);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
new file mode 100644
index 0000000..ed9d338
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPMultipleAttributesTest.java
@@ -0,0 +1,112 @@
+package org.keycloak.testsuite.federation;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPMultipleAttributesTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static UserFederationProviderModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            Map<String,String> ldapConfig = ldapRule.getConfig();
+            ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
+
+            ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
+            FederationTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Test
+    public void testModel() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+
+            FederationTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441");
+
+            UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+            Assert.assertEquals("bwilson@keycloak.org", user.getEmail());
+            Assert.assertEquals("Bruce", user.getFirstName());
+
+            // There are 2 lastnames in ldif
+            Assert.assertTrue("Wilson".equals(user.getLastName()) || "Schneider".equals(user.getLastName()));
+
+            // Actually there are 2 postalCodes
+            List<String> postalCodes = user.getAttribute("postal_code");
+            assertPostalCodes(postalCodes, "88441", "77332");
+
+            postalCodes.remove("77332");
+            user.setAttribute("postal_code", postalCodes);
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+            List<String> postalCodes = user.getAttribute("postal_code");
+            assertPostalCodes(postalCodes, "88441");
+
+            postalCodes.add("77332");
+            user.setAttribute("postal_code", postalCodes);
+            assertPostalCodes(user.getAttribute("postal_code"), "88441", "77332");
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    private void assertPostalCodes(List<String> postalCodes, String... expectedPostalCodes) {
+        if (expectedPostalCodes == null && postalCodes.isEmpty()) {
+            return;
+        }
+
+
+        Assert.assertEquals(expectedPostalCodes.length, postalCodes.size());
+        for (String expected : expectedPostalCodes) {
+            if (!postalCodes.contains(expected)) {
+                Assert.fail("postalCode '" + expected + "' not in postalCodes: " + postalCodes);
+            }
+        }
+    }
+
+
+
+}
+
+
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
index f8a6944..4802105 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
@@ -16,7 +16,7 @@ import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
-import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
 import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
 import org.keycloak.models.AccountRoles;
@@ -329,7 +329,7 @@ public class LDAPRoleMappingsTest {
     }
 
     private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) {
-        LDAPIdentityQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
+        LDAPQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
         LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
         Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName);
         ldapQuery.where(roleNameCondition);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
index bab44c1..b463bcd 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
@@ -126,8 +126,8 @@ public class SyncProvidersTest {
             FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126");
             LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5");
             // NOTE: Changing LDAP attributes directly here
-            ldapUser5.setAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
-            ldapUser5.setAttribute(LDAPConstants.POSTAL_CODE, "521");
+            ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
+            ldapUser5.setSingleAttribute(LDAPConstants.POSTAL_CODE, "521");
             ldapFedProvider.getLdapIdentityStore().update(ldapUser5);
 
             // Assert still old users in local provider
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
index b8e2453..af6750f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
@@ -54,7 +54,7 @@ public class EmailTest {
             UserModel user = manager.getSession().users().addUser(appRealm, "login-test");
             user.setEmail("login@test.com");
             user.setEnabled(true);
-            user.setAttribute(UserModel.LOCALE, "de");
+            user.setSingleAttribute(UserModel.LOCALE, "de");
 
             UserCredentialModel creds = new UserCredentialModel();
             creds.setType(CredentialRepresentation.PASSWORD);
@@ -91,7 +91,7 @@ public class EmailTest {
         keycloakRule.update(new KeycloakRule.KeycloakSetup() {
             @Override
             public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
-                manager.getSession().users().getUserByUsername("login-test", appRealm).setAttribute(UserModel.LOCALE, "en");
+                manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "en");
             }
         });
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 8640d8e..57b4ce9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -25,7 +25,6 @@ import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -159,7 +158,7 @@ public class AdapterTest extends AbstractModelTest {
         test1CreateRealm();
 
         UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
-        user.setAttribute("attr1", "val1");
+        user.setSingleAttribute("attr1", "val1");
         user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
 
         RoleModel testRole = realmModel.addRole("test");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 276698d..e28d2b3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -140,6 +140,22 @@ public class ImportTest extends AbstractModelTest {
         Assert.assertEquals(1, appRoles.size());
         Assert.assertEquals("app-admin", appRoles.iterator().next().getName());
 
+        // Test attributes
+        Map<String, List<String>> attrs = wburke.getAttributes();
+        Assert.assertEquals(1, attrs.size());
+        List<String> attrVals = attrs.get("email");
+        Assert.assertEquals(1, attrVals.size());
+        Assert.assertEquals("bburke@redhat.com", attrVals.get(0));
+
+        attrs = admin.getAttributes();
+        Assert.assertEquals(2, attrs.size());
+        attrVals = attrs.get("key1");
+        Assert.assertEquals(1, attrVals.size());
+        Assert.assertEquals("val1", attrVals.get(0));
+        attrVals = attrs.get("key2");
+        Assert.assertEquals(2, attrVals.size());
+        Assert.assertTrue(attrVals.contains("val21") && attrVals.contains("val22"));
+
         // Test client
         ClientModel oauthClient = realm.getClientByClientId("oauthclient");
         Assert.assertEquals("clientpassword", oauthClient.getSecret());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index d9e1aa5..ff74293 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -8,6 +8,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -137,6 +138,61 @@ public class UserModelTest extends AbstractModelTest {
         Assert.assertTrue(user.getRequiredActions().isEmpty());
     }
 
+    @Test
+    public void testUserMultipleAttributes() throws Exception {
+        RealmModel realm = realmManager.createRealm("original");
+        UserModel user = session.users().addUser(realm, "user");
+
+        user.setSingleAttribute("key1", "value1");
+        List<String> attrVals = new ArrayList<>(Arrays.asList( "val21", "val22" ));
+        user.setAttribute("key2", attrVals);
+
+        commit();
+
+        // Test read attributes
+        realm = realmManager.getRealmByName("original");
+        user = session.users().getUserByUsername("user", realm);
+
+        attrVals = user.getAttribute("key1");
+        Assert.assertEquals(1, attrVals.size());
+        Assert.assertEquals("value1", attrVals.get(0));
+        Assert.assertEquals("value1", user.getFirstAttribute("key1"));
+
+        attrVals = user.getAttribute("key2");
+        Assert.assertEquals(2, attrVals.size());
+        Assert.assertTrue(attrVals.contains("val21"));
+        Assert.assertTrue(attrVals.contains("val22"));
+
+        attrVals = user.getAttribute("key3");
+        Assert.assertTrue(attrVals.isEmpty());
+        Assert.assertNull(user.getFirstAttribute("key3"));
+
+        Map<String, List<String>> allAttrVals = user.getAttributes();
+        Assert.assertEquals(2, allAttrVals.size());
+        Assert.assertEquals(allAttrVals.get("key1"), user.getAttribute("key1"));
+        Assert.assertEquals(allAttrVals.get("key2"), user.getAttribute("key2"));
+
+        // Test searching
+        Map<String, String> attributes = new HashMap<String, String>();
+        attributes.put("key2", "val22");
+        List<UserModel> users = session.users().searchForUserByAttributes(attributes, realm);
+        Assert.assertEquals(1, users.size());
+        Assert.assertEquals(users.get(0), user);
+
+        // Test remove and rewrite attribute
+        user.removeAttribute("key1");
+        user.setSingleAttribute("key2", "val23");
+
+        commit();
+
+        realm = realmManager.getRealmByName("original");
+        user = session.users().getUserByUsername("user", realm);
+        Assert.assertNull(user.getFirstAttribute("key1"));
+        attrVals = user.getAttribute("key2");
+        Assert.assertEquals(1, attrVals.size());
+        Assert.assertEquals("val23", attrVals.get(0));
+    }
+
     public static void assertEquals(UserModel expected, UserModel actual) {
         Assert.assertEquals(expected.getUsername(), actual.getUsername());
         Assert.assertEquals(expected.getFirstName(), actual.getFirstName());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 0920be5..c92c162 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -616,12 +616,12 @@ public class AccessTokenTest {
             KeycloakSession session = keycloakRule.startSession();
             RealmModel realm = session.realms().getRealmByName("test");
             UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
-            user.setAttribute("street", "5 Yawkey Way");
-            user.setAttribute("locality", "Boston");
-            user.setAttribute("region", "MA");
-            user.setAttribute("postal_code", "02115");
-            user.setAttribute("country", "USA");
-            user.setAttribute("phone", "617-777-6666");
+            user.setSingleAttribute("street", "5 Yawkey Way");
+            user.setSingleAttribute("locality", "Boston");
+            user.setSingleAttribute("region", "MA");
+            user.setSingleAttribute("postal_code", "02115");
+            user.setSingleAttribute("country", "USA");
+            user.setSingleAttribute("phone", "617-777-6666");
             ClientModel app = realm.getClientByClientId("test-app");
             ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true);
             app.addProtocolMapper(mapper);
diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif
index de41e19..b04f081 100644
--- a/testsuite/integration/src/test/resources/ldap/users.ldif
+++ b/testsuite/integration/src/test/resources/ldap/users.ldif
@@ -38,6 +38,7 @@ objectclass: inetOrgPerson
 uid: bwilson
 cn: Bruce
 sn: Wilson
+sn: Schneider
 mail: bwilson@keycloak.org
 postalCode: 88441
 postalCode: 77332
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index 0e41313..00336c8 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -82,6 +82,15 @@
         {
             "username": "admin",
             "enabled": true,
+            "attributes": {
+                "key1": [
+                    "val1"
+                ],
+                "key2": [
+                    "val21",
+                    "val22"
+                ]
+            },
             "credentials": [
                 {
                     "type": "password",