keycloak-uncached

Merge pull request #3111 from patriot1burke/master user

8/3/2016 10:27:30 PM

Changes

Details

diff --git a/examples/providers/user-storage-jpa/assembly.xml b/examples/providers/user-storage-jpa/assembly.xml
new file mode 100755
index 0000000..4a3e1f9
--- /dev/null
+++ b/examples/providers/user-storage-jpa/assembly.xml
@@ -0,0 +1,45 @@
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<assembly>
+    <id>example-module</id>
+
+    <formats>
+        <format>dir</format>
+    </formats>
+
+    <includeBaseDirectory>false</includeBaseDirectory>
+
+    <fileSets>
+        <fileSet>
+            <directory>target</directory>
+            <outputDirectory>system/layers/keycloak/org/keycloak/examples/jpa-example/main</outputDirectory>
+            <filtered>true</filtered>
+            <includes>
+                <include>user-storage-jpa-example.jar</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>.</directory>
+            <outputDirectory>system/layers/keycloak/org/keycloak/examples/jpa-example/main</outputDirectory>
+            <filtered>true</filtered>
+            <includes>
+                <include>module.xml</include>
+            </includes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/examples/providers/user-storage-jpa/module.xml b/examples/providers/user-storage-jpa/module.xml
new file mode 100644
index 0000000..ffe4f83
--- /dev/null
+++ b/examples/providers/user-storage-jpa/module.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+  ~ and other contributors as indicated by the @author tags.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.examples.jpa-example">
+    <properties>
+        <property name="jboss.api" value="private"/>
+    </properties>
+
+    <resources>
+        <resource-root path="user-storage-jpa-example.jar"/>
+    </resources>
+
+    <dependencies>
+        <module name="org.keycloak.keycloak-common"/>
+        <module name="org.keycloak.keycloak-core"/>
+        <module name="org.keycloak.keycloak-server-spi"/>
+        <module name="org.keycloak.keycloak-model-jpa"/>
+        <module name="javax.persistence.api"/>
+        <module name="org.jboss.logging"/>
+        <module name="org.javassist"/>
+        <module name="org.hibernate" services="import"/>
+        <module name="org.bouncycastle" />
+        <module name="javax.api"/>
+    </dependencies>
+
+</module>
diff --git a/examples/providers/user-storage-jpa/pom.xml b/examples/providers/user-storage-jpa/pom.xml
index b025900..c1c38d2 100755
--- a/examples/providers/user-storage-jpa/pom.xml
+++ b/examples/providers/user-storage-jpa/pom.xml
@@ -74,6 +74,30 @@
                     <target>1.8</target>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>assemble</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>assembly.xml</descriptor>
+                            </descriptors>
+                            <recompressZippedFiles>true</recompressZippedFiles>
+                            <finalName>modules</finalName>
+                            <appendAssemblyId>false</appendAssemblyId>
+                            <outputDirectory>${project.build.directory}</outputDirectory>
+                            <workDirectory>${project.build.directory}/assembly/work</workDirectory>
+                            <tarLongFileMode>gnu</tarLongFileMode>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProvider.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProvider.java
index 0705333..f94ded8 100644
--- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProvider.java
+++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProvider.java
@@ -16,27 +16,40 @@
  */
 package org.keycloak.examples.storage.user;
 
+import org.jboss.logging.Logger;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.user.UserCredentialValidatorProvider;
 import org.keycloak.storage.user.UserLookupProvider;
+import org.keycloak.storage.user.UserQueryProvider;
 import org.keycloak.storage.user.UserRegistrationProvider;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ExampleUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserRegistrationProvider {
+public class ExampleUserStorageProvider implements UserStorageProvider,
+        UserLookupProvider,
+        UserRegistrationProvider,
+        UserCredentialValidatorProvider,
+        UserQueryProvider {
+    private static final Logger logger = Logger.getLogger(ExampleUserStorageProvider.class);
     protected EntityManager em;
     protected ComponentModel model;
     protected KeycloakSession session;
@@ -69,18 +82,27 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
 
     @Override
     public UserModel getUserById(String id, RealmModel realm) {
+        logger.info("getUserById: " + id);
         String persistenceId = StorageId.externalId(id);
         UserEntity entity = em.find(UserEntity.class, persistenceId);
-        if (entity == null) return null;
+        if (entity == null) {
+            logger.info("could not find user by id: " + id);
+            return null;
+        }
         return new UserAdapter(session, realm, model, entity);
     }
 
     @Override
     public UserModel getUserByUsername(String username, RealmModel realm) {
+        logger.info("getUserByUsername: " + username);
         TypedQuery<UserEntity> query = em.createNamedQuery("getUserByUsername", UserEntity.class);
         query.setParameter("username", username);
         List<UserEntity> result = query.getResultList();
-        if (result.isEmpty()) return null;
+        if (result.isEmpty()) {
+            logger.info("could not find username: " + username);
+            return null;
+        }
+
         return new UserAdapter(session, realm, model, result.get(0));
     }
 
@@ -99,6 +121,7 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
         entity.setId(KeycloakModelUtils.generateId());
         entity.setUsername(username);
         em.persist(entity);
+        logger.info("added user: " + username);
         return new UserAdapter(session, realm, model, entity);
     }
 
@@ -115,4 +138,91 @@ public class ExampleUserStorageProvider implements UserStorageProvider, UserLook
     public void grantToAllUsers(RealmModel realm, RoleModel role) {
 
     }
+
+    @Override
+    public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
+        // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+        // If we override getCredentialsDirectly/updateCredentialsDirectly
+        // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+        // If you don't like this workaround, you can query the database every time to validate the password
+        for (UserCredentialModel cred : input) {
+            if (!UserCredentialModel.PASSWORD.equals(cred.getType())) return false;
+            if (!cred.getValue().equals(user.getFirstAttribute("password"))) return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int getUsersCount(RealmModel realm) {
+        Object count = em.createNamedQuery("getUserCount")
+                .getSingleResult();
+        return ((Number)count).intValue();
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm) {
+        return getUsers(realm, -1, -1);
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
+
+        TypedQuery<UserEntity> query = em.createNamedQuery("getAllUsers", UserEntity.class);
+        if (firstResult != -1) {
+            query.setFirstResult(firstResult);
+        }
+        if (maxResults != -1) {
+            query.setMaxResults(maxResults);
+        }
+        List<UserEntity> results = query.getResultList();
+        List<UserModel> users = new LinkedList<>();
+        for (UserEntity entity : results) users.add(new UserAdapter(session, realm, model, entity));
+        return users;
+    }
+
+    @Override
+    public List<UserModel> searchForUser(String search, RealmModel realm) {
+        return searchForUser(search, realm, -1, -1);
+    }
+
+    @Override
+    public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
+        TypedQuery<UserEntity> query = em.createNamedQuery("searchForUser", UserEntity.class);
+        query.setParameter("search", "%" + search.toLowerCase() + "%");
+        if (firstResult != -1) {
+            query.setFirstResult(firstResult);
+        }
+        if (maxResults != -1) {
+            query.setMaxResults(maxResults);
+        }
+        List<UserEntity> results = query.getResultList();
+        List<UserModel> users = new LinkedList<>();
+        for (UserEntity entity : results) users.add(new UserAdapter(session, realm, model, entity));
+        return users;
+    }
+
+    @Override
+    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
+        return Collections.EMPTY_LIST;
+    }
 }
diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProviderFactory.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProviderFactory.java
index 963a558..f282df2 100644
--- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProviderFactory.java
+++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/ExampleUserStorageProviderFactory.java
@@ -16,25 +16,39 @@
  */
 package org.keycloak.examples.storage.user;
 
+import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
+import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
+import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
+import org.hibernate.jpa.boot.spi.Bootstrap;
 import org.keycloak.Config;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.connections.jpa.JndiEntityManagerLookup;
+import org.keycloak.connections.jpa.JpaKeycloakTransaction;
+import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.storage.UserStorageProviderFactory;
 
 import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Persistence;
+import javax.persistence.spi.PersistenceUnitTransactionType;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
 public class ExampleUserStorageProviderFactory implements UserStorageProviderFactory<ExampleUserStorageProvider> {
-    protected String jndiName = "java:jboss/ExampleUserEntityManagerFactory";
+
+    EntityManagerFactory emf = null;
 
     @Override
     public ExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
-        EntityManager em = JndiEntityManagerLookup.getSessionEntityManager(session, jndiName);
+        EntityManager em = emf.createEntityManager();
+        session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
         return new ExampleUserStorageProvider(em, model, session);
     }
 
@@ -50,11 +64,27 @@ public class ExampleUserStorageProviderFactory implements UserStorageProviderFac
 
     @Override
     public void postInit(KeycloakSessionFactory factory) {
+        //emf = Persistence.createEntityManagerFactory("user-storage-jpa-example");
+        emf = createEntityManagerFactory("user-storage-jpa-example");
+        //emf = Bootstrap.getEntityManagerFactoryBuilder()
+
+    }
 
+    public static EntityManagerFactory createEntityManagerFactory(String unitName) {
+        PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(ExampleUserStorageProviderFactory.class.getClassLoader()), PersistenceUnitTransactionType.RESOURCE_LOCAL);
+        List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(new HashMap());
+        for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
+            if (persistenceUnit.getName().equals(unitName)) {
+                return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, new HashMap(), ExampleUserStorageProviderFactory.class.getClassLoader()).build();
+            }
+        }
+        throw new RuntimeException("Persistence unit '" + unitName + "' not found");
     }
 
+
     @Override
     public void close() {
+        emf.close();
 
     }
 }
diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java
index f851904..f4ff5a7 100644
--- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java
+++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java
@@ -16,11 +16,13 @@
  */
 package org.keycloak.examples.storage.user;
 
+import org.jboss.logging.Logger;
 import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
 
@@ -33,6 +35,7 @@ import java.util.Map;
  * @version $Revision: 1 $
  */
 public class UserAdapter extends AbstractUserAdapterFederatedStorage {
+    private static final Logger logger = Logger.getLogger(ExampleUserStorageProvider.class);
     protected UserEntity entity;
     protected String keycloakId;
 
@@ -81,6 +84,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
     public void setSingleAttribute(String name, String value) {
         if (name.equals("phone")) {
             entity.setPhone(value);
+        } else if (name.equals("password")) {
+            // ignore
+
+            // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+            // If we override getCredentialsDirectly/updateCredentialsDirectly
+            // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+            // If you don't like this workaround, you can query the database every time to validate the password
         } else {
             super.setSingleAttribute(name, value);
         }
@@ -90,6 +100,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
     public void removeAttribute(String name) {
         if (name.equals("phone")) {
             entity.setPhone(null);
+        } else if (name.equals("password")) {
+            // ignore
+
+            // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+            // If we override getCredentialsDirectly/updateCredentialsDirectly
+            // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+            // If you don't like this workaround, you can query the database every time to validate the password
         } else {
             super.removeAttribute(name);
         }
@@ -99,6 +116,13 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
     public void setAttribute(String name, List<String> values) {
         if (name.equals("phone")) {
             entity.setPhone(values.get(0));
+        } else if (name.equals("password")) {
+            // ignore
+
+            // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+            // If we override getCredentialsDirectly/updateCredentialsDirectly
+            // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+            // If you don't like this workaround, you can query the database every time to validate the password
         } else {
             super.setAttribute(name, values);
         }
@@ -108,6 +132,12 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
     public String getFirstAttribute(String name) {
         if (name.equals("phone")) {
             return entity.getPhone();
+        } else if (name.equals("password")) {
+            // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+            // If we override getCredentialsDirectly/updateCredentialsDirectly
+            // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+            // If you don't like this workaround, you can query the database every time to validate the password
+            return entity.getPassword();
         } else {
             return super.getFirstAttribute(name);
         }
@@ -119,6 +149,12 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
         MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();
         all.putAll(attrs);
         all.add("phone", entity.getPhone());
+
+        // having a "password" attribute is a workaround so that passwords can be cached.  All done for performance reasons...
+        // If we override getCredentialsDirectly/updateCredentialsDirectly
+        // then the realm passsword policy will/may try and overwrite the plain text password with a hash.
+        // If you don't like this workaround, you can query the database every time to validate the password
+        all.add("password", entity.getPassword());
         return all;
     }
 
diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserEntity.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserEntity.java
index c9227d2..299e900 100644
--- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserEntity.java
+++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserEntity.java
@@ -28,6 +28,10 @@ import javax.persistence.NamedQuery;
 @NamedQueries({
         @NamedQuery(name="getUserByUsername", query="select u from UserEntity u where u.username = :username"),
         @NamedQuery(name="getUserByEmail", query="select u from UserEntity u where u.email = :email"),
+        @NamedQuery(name="getUserCount", query="select count(u) from UserEntity u"),
+        @NamedQuery(name="getAllUsers", query="select u from UserEntity u"),
+        @NamedQuery(name="searchForUser", query="select u from UserEntity u where " +
+                "( lower(u.username) like :search or u.email like :search ) order by u.username"),
 })
 @Entity
 public class UserEntity {
diff --git a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
index 5af362d..d082c23 100644
--- a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
+++ b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
@@ -4,13 +4,13 @@
              xsi:schemaLocation="
         http://java.sun.com/xml/ns/persistence
         http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
-    <persistence-unit name="primary">
-        <non-jta-data-source>java:jboss/datasources/ExampleUserDS</non-jta-data-source>
+    <persistence-unit name="user-storage-jpa-example" transaction-type="RESOURCE_LOCAL">
+        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
+        <non-jta-data-source>java:jboss/datasources/ExampleDS</non-jta-data-source>
 
         <class>org.keycloak.examples.storage.user.UserEntity</class>
 
         <properties>
-            <property name="jboss.entity.manager.factory.jndi.name" value="java:jboss/ExampleUserEntityManagerFactory" />
             <property name="hibernate.hbm2ddl.auto" value="update" />
             <property name="hibernate.show_sql" value="false" />
         </properties>
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 3016ae8..8a62c33 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -393,13 +393,13 @@ public class UserCacheSession implements CacheUserProvider {
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
-        return getDelegate().searchForUserByAttributes(attributes, realm);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
+        return getDelegate().searchForUser(attributes, realm);
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
-        return getDelegate().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+        return getDelegate().searchForUser(attributes, realm, firstResult, maxResults);
     }
 
     @Override
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index a928e58..0d938d6 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -49,6 +49,7 @@ import javax.persistence.TypedQuery;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -547,7 +548,7 @@ public class JpaUserProvider implements UserProvider {
             query.setMaxResults(maxResults);
         }
         List<UserEntity> results = query.getResultList();
-        List<UserModel> users = new ArrayList<UserModel>();
+        List<UserModel> users = new LinkedList<>();
         for (UserEntity entity : results) users.add(new UserAdapter(session, realm, em, entity));
         return users;
     }
@@ -564,7 +565,7 @@ public class JpaUserProvider implements UserProvider {
         }
         List<UserEntity> results = query.getResultList();
 
-        List<UserModel> users = new ArrayList<UserModel>();
+        List<UserModel> users = new LinkedList<>();
         for (UserEntity user : results) {
             users.add(new UserAdapter(session, realm, em, user));
         }
@@ -588,18 +589,18 @@ public class JpaUserProvider implements UserProvider {
             query.setMaxResults(maxResults);
         }
         List<UserEntity> results = query.getResultList();
-        List<UserModel> users = new ArrayList<UserModel>();
+        List<UserModel> users = new LinkedList<>();
         for (UserEntity entity : results) users.add(new UserAdapter(session, realm, em, entity));
         return users;
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
-        return searchForUserByAttributes(attributes, realm, -1, -1);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
+        return searchForUser(attributes, realm, -1, -1);
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
         StringBuilder builder = new StringBuilder("select u from UserEntity u where u.realmId = :realmId");
         for (Map.Entry<String, String> entry : attributes.entrySet()) {
             String attribute = null;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 96d1f91..c0537ba 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -253,12 +253,12 @@ public class MongoUserProvider implements UserProvider {
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
-        return searchForUserByAttributes(attributes, realm, -1, -1);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
+        return searchForUser(attributes, realm, -1, -1);
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
         QueryBuilder queryBuilder = new QueryBuilder()
                 .and("realmId").is(realm.getId());
 
diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
index c48ae4d..cb6867a 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -404,17 +404,17 @@ public class UserFederationManager implements UserProvider {
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
-        return searchForUserByAttributes(attributes, realm, 0, Integer.MAX_VALUE - 1);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
+        return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(final Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> searchForUser(final Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
         federationLoad(realm, attributes);
         return query(new PaginatedQuery() {
             @Override
             public List<UserModel> query(RealmModel realm, int first, int max) {
-                return session.userStorage().searchForUserByAttributes(attributes, realm, first, max);
+                return session.userStorage().searchForUser(attributes, realm, first, max);
             }
         }, realm, firstResult, maxResults);
     }
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
index ceb4fe0..877b09f 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
@@ -29,24 +29,79 @@ import java.util.Map;
  */
 public interface UserQueryProvider {
 
-    // Service account is included for counts
     int getUsersCount(RealmModel realm);
 
     List<UserModel> getUsers(RealmModel realm);
-    List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm);
-
     List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults);
-    List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
-    List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults);
 
+    /**
+     * Search for users with username, email or first + last name that is like search string.
+     *
+     * If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
+     *
+     * This method is used by the admin console search box
+     *
+     * @param search
+     * @param realm
+     * @return
+     */
+    List<UserModel> searchForUser(String search, RealmModel realm);
 
+    /**
+     * Search for users with username, email or first + last name that is like search string.
+     *
+     * If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
+     *
+     * This method is used by the admin console search box
+     *
+     * @param search
+     * @param realm
+     * @param firstResult
+     * @param maxResults
+     * @return
+     */
+    List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
 
+    /**
+     * Search for user by parameter.  Valid parameters are:
+     * "first" - first name
+     * "last" - last name
+     * "email" - email
+     * "username" - username
+     *
+     * If possible, implementations should treat the parameter values as partial match patterns i.e. in RDMBS terms use LIKE.
+     *
+     * This method is used by the REST API when querying users.
+     *
+     *
+     * @param params
+     * @param realm
+     * @return
+     */
+    List<UserModel> searchForUser(Map<String, String> params, RealmModel realm);
 
+    /**
+     * Search for user by parameter.  Valid parameters are:
+     * "first" - first name
+     * "last" - last name
+     * "email" - email
+     * "username" - username
+     *
+     * If possible, implementations should treat the parameter values as patterns i.e. in RDMBS terms use LIKE.
+     * This method is used by the REST API when querying users.
+     *
+     *
+     * @param params
+     * @param realm
+     * @param firstResult
+     * @param maxResults
+     * @return
+     */
+    List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults);
 
     List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
     List<UserModel> getGroupMembers(RealmModel realm, GroupModel group);
 
-    List<UserModel> searchForUser(String search, RealmModel realm);
 
     // Searching by UserModel.attribute (not property)
     List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm);
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java
index 72906b0..1a1fca5 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -45,7 +45,6 @@ import org.keycloak.storage.federated.UserFederatedStorageProvider;
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -387,15 +386,15 @@ public class UserStorageManager implements UserProvider {
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
-        return searchForUserByAttributes(attributes, realm, 0, Integer.MAX_VALUE - 1);
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
+        return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
         return query((provider, first, max) -> {
             if (provider instanceof UserQueryProvider) {
-                return ((UserQueryProvider)provider).searchForUserByAttributes(attributes, realm, first, max);
+                return ((UserQueryProvider)provider).searchForUser(attributes, realm, first, max);
 
             }
             return Collections.EMPTY_LIST;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 1a1d2bf..e1c2363 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -95,6 +95,7 @@ public class ServerInfoAdminResource {
     }
 
     private void setProviders(ServerInfoRepresentation info) {
+        info.setComponentTypes(new HashMap<>());
         LinkedHashMap<String, SpiInfoRepresentation> spiReps = new LinkedHashMap<>();
 
         List<Spi> spis = new LinkedList<>(session.getKeycloakSessionFactory().getSpis());
@@ -115,7 +116,6 @@ public class ServerInfoAdminResource {
             Map<String, ProviderRepresentation> providers = new HashMap<>();
 
             if (providerIds != null) {
-                info.setComponentTypes(new HashMap<>());
                 for (String name : providerIds) {
                     ProviderRepresentation provider = new ProviderRepresentation();
                     ProviderFactory<?> pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name);
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 aa6aa68..367217c 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
@@ -46,7 +46,6 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.CredentialRepresentation;
@@ -682,7 +681,7 @@ public class UsersResource {
             if (username != null) {
                 attributes.put(UserModel.USERNAME, username);
             }
-            userModels = session.users().searchForUserByAttributes(attributes, realm, firstResult, maxResults);
+            userModels = session.users().searchForUser(attributes, realm, firstResult, maxResults);
         } else {
             userModels = session.users().getUsers(realm, firstResult, maxResults, false);
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProvider.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProvider.java
new file mode 100644
index 0000000..f7aba5c
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.federation.storage;
+
+import org.keycloak.hash.PasswordHashProvider;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PlainTextPasswordProvider implements PasswordHashProvider {
+    @Override
+    public UserCredentialValueModel encode(String rawPassword, int iterations) {
+        UserCredentialValueModel model = new UserCredentialValueModel();
+        model.setType(UserCredentialModel.PASSWORD);
+        model.setValue(rawPassword);
+        model.setAlgorithm("text");
+        return model;
+    }
+
+    @Override
+    public boolean verify(String rawPassword, UserCredentialValueModel credential) {
+        return rawPassword.equals(credential.getValue());
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProviderFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProviderFactory.java
new file mode 100644
index 0000000..cdb324c
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/PlainTextPasswordProviderFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.federation.storage;
+
+import org.keycloak.Config;
+import org.keycloak.hash.PasswordHashProvider;
+import org.keycloak.hash.PasswordHashProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PlainTextPasswordProviderFactory implements PasswordHashProviderFactory {
+    @Override
+    public PasswordHashProvider create(KeycloakSession session) {
+        return new PlainTextPasswordProvider();
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return "text";
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java
index a53479c..7f72e8e 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java
@@ -22,6 +22,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
@@ -30,6 +31,7 @@ import org.keycloak.storage.user.UserCredentialValidatorProvider;
 import org.keycloak.storage.user.UserLookupProvider;
 import org.keycloak.storage.user.UserRegistrationProvider;
 
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -38,7 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserCredentialValidatorProvider, UserRegistrationProvider {
+public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider {
 
     protected Map<String, String> userPasswords;
     protected ComponentModel model;
@@ -79,11 +81,33 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider, 
             @Override
             public void updateCredential(UserCredentialModel cred) {
                 if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
-                    userPasswords.put(username, cred.getValue());
+                    userPasswords.put(getUsername(), cred.getValue());
                 } else {
                     super.updateCredential(cred);
                 }
             }
+
+            @Override
+            public List<UserCredentialValueModel> getCredentialsDirectly() {
+                UserCredentialValueModel pw = new UserCredentialValueModel();
+                pw.setId(getId());
+                pw.setType(UserCredentialModel.PASSWORD);
+                pw.setAlgorithm("text");
+                pw.setValue(userPasswords.get(getUsername()));
+                List<UserCredentialValueModel> creds = new LinkedList<>();
+                creds.addAll(super.getCredentialsDirectly());
+                creds.add(pw);
+                return creds;
+            }
+
+            @Override
+            public void updateCredentialDirectly(UserCredentialValueModel cred) {
+                if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
+                    //userPasswords.put(getUsername(), cred.getValue());
+                } else {
+                    super.updateCredentialDirectly(cred);
+                }
+            }
         };
     }
 
@@ -131,19 +155,6 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider, 
     }
 
     @Override
-    public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
-        for (UserCredentialModel cred : input) {
-            if (!cred.getType().equals(UserCredentialModel.PASSWORD)) return false;
-            String password = (String)userPasswords.get(user.getUsername());
-            if (password == null) return false;
-            if (!password.equals(cred.getValue())) return false;
-        }
-        return true;
-    }
-
-
-
-    @Override
     public void close() {
         closings.incrementAndGet();
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java
index 489dccd..c297aca 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java
@@ -141,7 +141,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm) {
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
         return Collections.EMPTY_LIST;
     }
 
@@ -178,7 +178,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP
     }
 
     @Override
-    public List<UserModel> searchForUserByAttributes(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
+    public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
         if (attributes.size() != 1) return Collections.EMPTY_LIST;
         String username = attributes.get(UserModel.USERNAME);
         if (username == null) return Collections.EMPTY_LIST;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
index 3e75d64..3c3a6a0 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
@@ -108,7 +108,6 @@ public class UserStorageTest {
         Assert.assertEquals("Invalid username or password.", loginPage.getError());
     }
 
-
     @Test
     public void testLoginSuccess() {
         loginSuccessAndLogout("tbrady", "goat");
@@ -258,12 +257,14 @@ public class UserStorageTest {
         user.updateCredential(UserCredentialModel.password("password"));
         keycloakRule.stopSession(session, true);
         loginSuccessAndLogout("memuser", "password");
+        loginSuccessAndLogout("memuser", "password");
+        loginSuccessAndLogout("memuser", "password");
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
         user = session.users().getUserByUsername("memuser", realm);
         Assert.assertEquals(memoryProvider.getId(), StorageId.resolveProviderId(user));
-        Assert.assertEquals(0, user.getCredentialsDirectly().size());
+        Assert.assertEquals(1, user.getCredentialsDirectly().size());
         session.users().removeUser(realm, user);
         Assert.assertNull(session.users().getUserByUsername("memuser", realm));
         keycloakRule.stopSession(session, true);
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 01f99fb..4b3f78e 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
@@ -66,20 +66,20 @@ public class UserModelTest extends AbstractModelTest {
 
         Map<String, String> attributes = new HashMap<String, String>();
         attributes.put(UserModel.LAST_NAME, "last-name");
-        List<UserModel> search = session.users().searchForUserByAttributes(attributes, realm);
+        List<UserModel> search = session.users().searchForUser(attributes, realm);
         Assert.assertEquals(search.size(), 1);
         Assert.assertEquals(search.get(0).getUsername(), "user");
 
         attributes.clear();
         attributes.put(UserModel.EMAIL, "email");
-        search = session.users().searchForUserByAttributes(attributes, realm);
+        search = session.users().searchForUser(attributes, realm);
         Assert.assertEquals(search.size(), 1);
         Assert.assertEquals(search.get(0).getUsername(), "user");
 
         attributes.clear();
         attributes.put(UserModel.LAST_NAME, "last-name");
         attributes.put(UserModel.EMAIL, "email");
-        search = session.users().searchForUserByAttributes(attributes, realm);
+        search = session.users().searchForUser(attributes, realm);
         Assert.assertEquals(search.size(), 1);
         Assert.assertEquals(search.get(0).getUsername(), "user");
     }
diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.hash.PasswordHashProviderFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.hash.PasswordHashProviderFactory
new file mode 100644
index 0000000..9b9b525
--- /dev/null
+++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.hash.PasswordHashProviderFactory
@@ -0,0 +1 @@
+org.keycloak.testsuite.federation.storage.PlainTextPasswordProviderFactory
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 6165420..a867747 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -597,6 +597,7 @@ client-template.description.tooltip=Description of the client template
 client-template.protocol.tooltip=Which SSO protocol configuration is being supplied by this client template
 
 add-user-federation-provider=Add user federation provider
+add-user-storage-provider=Add user storage provider
 required-settings=Required Settings
 provider-id=Provider ID
 console-display-name=Console Display Name
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index 2609e81..c2c44c0 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1350,6 +1350,44 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'UserStorageCtrl'
         })
+        .when('/create/user-storage/:realm/providers/:provider', {
+            templateUrl : resourceUrl + '/partials/user-storage-generic.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                instance : function() {
+                    return {
+
+                    };
+                },
+                providerId : function($route) {
+                    return $route.current.params.provider;
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'GenericUserStorageCtrl'
+        })
+        .when('/realms/:realm/user-storage/providers/:provider/:componentId', {
+            templateUrl : resourceUrl + '/partials/user-storage-generic.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                instance : function(ComponentLoader) {
+                    return ComponentLoader();
+                },
+                providerId : function($route) {
+                    return $route.current.params.provider;
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'GenericUserStorageCtrl'
+        })
         .when('/realms/:realm/user-federation', {
             templateUrl : resourceUrl + '/partials/user-federation.html',
             resolve : {
@@ -2344,6 +2382,89 @@ module.directive('kcProviderConfig', function ($modal) {
     }
 });
 
+module.controller('ComponentRoleSelectorModalCtrl', function($scope, realm, config, configName, RealmRoles, Client, ClientRole, $modalInstance) {
+    $scope.selectedRealmRole = {
+        role: undefined
+    };
+    $scope.selectedClientRole = {
+        role: undefined
+    };
+    $scope.client = {
+        selected: undefined
+    };
+
+    $scope.selectRealmRole = function() {
+        config[configName][0] = $scope.selectedRealmRole.role.name;
+        $modalInstance.close();
+    }
+
+    $scope.selectClientRole = function() {
+        config[configName][0] = $scope.client.selected.clientId + "." + $scope.selectedClientRole.role.name;
+        $modalInstance.close();
+    }
+
+    $scope.cancel = function() {
+        $modalInstance.dismiss();
+    }
+
+    $scope.changeClient = function() {
+        if ($scope.client.selected) {
+            ClientRole.query({realm: realm.realm, client: $scope.client.selected.id}, function (data) {
+                $scope.clientRoles = data;
+            });
+        } else {
+            console.log('selected client was null');
+            $scope.clientRoles = null;
+        }
+
+    }
+    RealmRoles.query({realm: realm.realm}, function(data) {
+        $scope.realmRoles = data;
+    })
+    Client.query({realm: realm.realm}, function(data) {
+        $scope.clients = data;
+        if (data.length > 0) {
+            $scope.client.selected = data[0];
+            $scope.changeClient();
+        }
+    })
+});
+
+module.controller('ComponentConfigCtrl', function ($modal, $scope) {
+    $scope.openRoleSelector = function (configName, config) {
+        $modal.open({
+            templateUrl: resourceUrl + '/partials/modal/component-role-selector.html',
+            controller: 'ComponentRoleSelectorModalCtrl',
+            resolve: {
+                realm: function () {
+                    return $scope.realm;
+                },
+                config: function () {
+                    return config;
+                },
+                configName: function () {
+                    return configName;
+                }
+            }
+        })
+    }
+});
+module.directive('kcComponentConfig', function ($modal) {
+    return {
+        scope: {
+            config: '=',
+            properties: '=',
+            realm: '=',
+            clients: '=',
+            configName: '='
+        },
+        restrict: 'E',
+        replace: true,
+        controller: 'ComponentConfigCtrl',
+        templateUrl: resourceUrl + '/templates/kc-component-config.html'
+    }
+});
+
 /*
 *  Used to select the element (invoke $(elem).select()) on specified action list.
 *  Usages kc-select-action="click mouseover"
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 8f07ffd..82072ea 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -620,6 +620,123 @@ module.controller('UserStorageCtrl', function($scope, $location, $route, realm, 
     };
 });
 
+module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm, serverInfo, instance, providerId, Components) {
+    console.log('GenericUserFederationCtrl');
+    console.log('providerId: ' + providerId);
+    $scope.create = !instance.providerId;
+    console.log('create: ' + $scope.create);
+    var providers = serverInfo.componentTypes['org.keycloak.storage.UserStorageProvider'];
+    console.log('providers length ' + providers.length);
+    var providerFactory = null;
+    for (var i = 0; i < providers.length; i++) {
+        var p = providers[i];
+        console.log('provider: ' + p.id);
+        if (p.id == providerId) {
+            $scope.providerFactory = p;
+            providerFactory = p;
+            break;
+        }
+
+    }
+    $scope.provider = instance;
+
+    console.log("providerFactory: " + providerFactory.id);
+
+    function initUserStorageSettings() {
+        if ($scope.create) {
+            instance.name = providerFactory.id;
+            instance.providerId = providerFactory.id;
+            instance.providerType = 'org.keycloak.storage.UserStorageProvider';
+            instance.parentId = realm.id;
+            instance.config = {
+
+            };
+            instance.config['priority'] = ["0"];
+
+            if (providerFactory.properties) {
+
+                for (var i = 0; i < providerFactory.properties.length; i++) {
+                    var configProperty = providerFactory.properties[i];
+                    if (configProperty.defaultValue) {
+                        instance.config[configProperty.name] = [configProperty.defaultValue];
+                    } else {
+                        instance.config[configProperty.name] = [''];
+                    }
+
+                }
+            }
+
+        } else {
+            /*
+            console.log('Manage instance');
+            console.log(instance.name);
+            console.log(instance.providerId);
+            console.log(instance.providerType);
+            console.log(instance.parentId);
+            for (var k in instance.config) {
+                console.log('config[' + k + "] =");
+            }
+            */
+        }
+
+        $scope.changed = false;
+    }
+
+    initUserStorageSettings();
+    $scope.instance = angular.copy(instance);
+    $scope.realm = realm;
+
+     $scope.$watch('instance', function() {
+        if (!angular.equals($scope.instance, instance)) {
+            $scope.changed = true;
+        }
+
+    }, true);
+
+    $scope.save = function() {
+        $scope.changed = false;
+        if ($scope.create) {
+            Components.save({realm: realm.realm}, $scope.instance,  function (data, headers) {
+                var l = headers().location;
+                var id = l.substring(l.lastIndexOf("/") + 1);
+
+                $location.url("/realms/" + realm.realm + "/user-storage/providers/" + $scope.instance.providerId + "/" + id);
+                Notifications.success("The provider has been created.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
+            });
+        } else {
+            Components.update({realm: realm.realm,
+                    componentId: instance.id
+                },
+                $scope.instance,  function () {
+                    $route.reload();
+                    Notifications.success("The provider has been updated.");
+                }, function (errorResponse) {
+                    if (errorResponse.data && errorResponse.data['error_description']) {
+                        Notifications.error(errorResponse.data['error_description']);
+                    }
+                });
+        }
+    };
+
+    $scope.reset = function() {
+        initUserStorageSettings();
+        $scope.instance = angular.copy(instance);
+    };
+
+    $scope.cancel = function() {
+        if ($scope.create) {
+            $location.url("/realms/" + realm.realm + "/user-storage");
+        } else {
+            $route.reload();
+        }
+    };
+});
+
+
 module.controller('UserFederationCtrl', function($scope, $location, $route, realm, UserFederationProviders, UserFederationInstances, Notifications, Dialog) {
     console.log('UserFederationCtrl ++++****');
     $scope.realm = realm;
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index e949a9d..33fb7ac 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -126,6 +126,15 @@ module.factory('UserLoader', function(Loader, User, $route, $q) {
     });
 });
 
+module.factory('ComponentLoader', function(Loader, Components, $route, $q) {
+    return Loader.get(Components, function() {
+        return {
+            realm : $route.current.params.realm,
+            componentId: $route.current.params.componentId
+        }
+    });
+});
+
 module.factory('UserFederationInstanceLoader', function(Loader, UserFederationInstances, $route, $q) {
     return Loader.get(UserFederationInstances, function() {
         return {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage.html
index fb6b87a..156a1ff 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage.html
@@ -1,6 +1,6 @@
 <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
     <h1>
-        <span>{{:: 'user-federation' | translate}}</span>
+        <span>{{:: 'user-storage' | translate}}</span>
     </h1>
 
     <table class="table table-striped table-bordered">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
new file mode 100755
index 0000000..0a89ac8
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
@@ -0,0 +1,54 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/user-storage">{{:: 'user-storage' | translate}}</a></li>
+        <li data-ng-hide="create">{{instance.name|capitalize}}</li>
+        <li data-ng-show="create">{{:: 'add-user-storage-provider' | translate}}</li>
+    </ol>
+
+    <kc-tabs-user-storage></kc-tabs-user-storage>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+        <fieldset>
+            <legend><span class="text">{{:: 'required-settings' | translate}}</span></legend>
+            <div class="form-group clearfix" data-ng-show="!create">
+                <label class="col-md-2 control-label" for="providerId">{{:: 'provider-id' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="providerId" type="text" ng-model="instance.id" readonly>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="consoleDisplayName">{{:: 'console-display-name' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="consoleDisplayName" type="text" ng-model="instance.name" placeholder="{{:: 'defaults-to-id' | translate}}">
+                </div>
+                <kc-tooltip>{{:: 'console-display-name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="priority">{{:: 'priority' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="priority" type="text" ng-model="instance.config['priority'][0]">
+                </div>
+                <kc-tooltip>{{:: 'priority.tooltip' | translate}}</kc-tooltip>
+            </div>
+
+            <kc-component-config realm="realm" config="instance.config" properties="providerFactory.properties"></kc-component-config>
+
+        </fieldset>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
+                <button kc-save>{{:: 'save' | translate}}</button>
+                <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
+                <button kc-save  data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+                <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
new file mode 100755
index 0000000..97d8876
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
@@ -0,0 +1,43 @@
+<div>
+    <div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
+        <label class="col-md-2 control-label">{{:: option.label | translate}}</label>
+
+        <div class="col-md-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList' || option.type == 'Password' || option.type=='Script'">
+            <input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
+        </div>
+        <div class="col-md-6" data-ng-show="option.type == 'Password'">
+            <input class="form-control" type="password" data-ng-model="config[ option.name ][0]" >
+        </div>
+        <div class="col-md-6" data-ng-show="option.type == 'boolean'">
+            <input ng-model="config[ option.name ][0]" value="'true'" name="option.name" id="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+        </div>
+        <div class="col-md-6" data-ng-show="option.type == 'List'">
+            <select ng-model="config[ option.name ][0]" ng-options="data for data in option.defaultValue">
+                <option value="" selected> {{:: 'selectOne' | translate}} </option>
+            </select>
+        </div>
+        <div class="col-md-6" data-ng-show="option.type == 'Role'">
+            <div class="row">
+                <div class="col-md-8">
+                    <input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
+                </div>
+                <div class="col-md-2">
+                    <button type="submit" data-ng-click="openRoleSelector(option.name, config)" class="btn btn-default" tooltip-placement="top" tooltip-trigger="mouseover mouseout" tooltip="{{:: 'selectRole.tooltip' | translate}}">{{:: 'selectRole.label' | translate}}</button>
+                </div>
+            </div>
+        </div>
+        <div class="col-md-4" data-ng-show="option.type == 'ClientList'">
+            <select ng-model="config[ option.name ][0]" ng-options="client.clientId as client.clientId for client in clients">
+                <option value="" selected> {{:: 'selectOne' | translate}} </option>
+            </select>
+        </div>
+
+        <div class="col-md-6" data-ng-show="option.type == 'Script'">
+            <div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
+                {{config[option.name]}}
+            </div>
+        </div>
+
+        <kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
+    </div>
+</div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
index b27e7f3..926b43c 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
@@ -34,7 +34,7 @@
             <li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'roles' || path[2] == 'default-roles' || (path[1] == 'role' && path[3] != 'clients')) && 'active'"><a href="#/realms/{{realm.realm}}/roles"><i class="fa fa-tasks"></i> {{:: 'roles' | translate}}</a></li>
             <li data-ng-show="access.viewIdentityProviders" data-ng-class="(path[2] == 'identity-provider-settings' || path[2] == 'identity-provider-mappers') && 'active'"><a href="#/realms/{{realm.realm}}/identity-provider-settings"><i class="fa fa-exchange"></i> {{:: 'identity-providers' | translate}}</a></li>
             <li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-federation' || path[2] == 'user-federation') && 'active'"><a href="#/realms/{{realm.realm}}/user-federation"><i class="fa fa-database"></i> {{:: 'user-federation' | translate}}</a></li>
-            <!-- <li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-storage' || path[2] == 'user-storage') && 'active'"><a href="#/realms/{{realm.realm}}/user-storage"><i class="fa fa-database"></i> {{:: 'user-storage' | translate}}</a></li> -->
+            <li data-ng-show="access.viewUsers" data-ng-class="(path[1] == 'user-storage' || path[2] == 'user-storage') && 'active'"><a href="#/realms/{{realm.realm}}/user-storage"><i class="fa fa-database"></i> {{:: 'user-storage' | translate}}</a></li>
             <li data-ng-show="access.viewRealm" data-ng-class="(path[1] == 'authentication' || path[2] == 'authentication') && 'active'"><a href="#/realms/{{realm.realm}}/authentication/flows"><i class="fa fa-lock"></i> {{:: 'authentication' | translate}}</a></li>
         </ul>
     </div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-storage.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-storage.html
new file mode 100644
index 0000000..03e1ddc
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user-storage.html
@@ -0,0 +1,11 @@
+<div data-ng-controller="UserStorageTabCtrl">
+    <h1 data-ng-hide="create">
+        {{instance.name|capitalize}}
+        <i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserStorage()"></i>
+    </h1>
+    <h1 data-ng-show="create">{{:: 'add-user-storage-provider' | translate}}</h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/user-storage/providers/{{instance.providerName}}/{{instance.id}}">{{:: 'settings' | translate}}</a></li>
+    </ul>
+</div>