keycloak-aplcache

Changes

model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientIdentityProviderMappingEntity.java 141(+0 -141)

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index 77c2c45..83c4f8a 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -67,6 +67,7 @@ public class ClientRepresentation {
     private Boolean useTemplateMappers;
     private ResourceServerRepresentation authorizationSettings;
     private Map<String, Boolean> access;
+    protected String origin;
 
 
     public String getId() {
@@ -384,4 +385,19 @@ public class ClientRepresentation {
     public void setAccess(Map<String, Boolean> access) {
         this.access = access;
     }
+
+
+    /**
+     * Returns id of ClientStorageProvider that loaded this user
+     *
+     * @return NULL if user stored locally
+     */
+    public String getOrigin() {
+        return origin;
+    }
+
+    public void setOrigin(String origin) {
+        this.origin = origin;
+    }
+
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index ab73c94..cb5d060 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -41,6 +41,7 @@ import org.keycloak.authorization.store.ScopeStore;
 import org.keycloak.authorization.store.StoreFactory;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ModelException;
 import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
 import org.keycloak.models.cache.infinispan.authorization.entities.CachedPolicy;
 import org.keycloak.models.cache.infinispan.authorization.entities.CachedResource;
@@ -64,6 +65,7 @@ import org.keycloak.models.cache.infinispan.authorization.events.ScopeRemovedEve
 import org.keycloak.models.cache.infinispan.authorization.events.ScopeUpdatedEvent;
 import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
+import org.keycloak.storage.StorageId;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -348,6 +350,9 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
     protected class ResourceServerCache implements ResourceServerStore {
         @Override
         public ResourceServer create(String clientId) {
+            if (!StorageId.isLocalStorage(clientId)) {
+                throw new ModelException("Creating resource server from federated ClientModel not supported");
+            }
             ResourceServer server = getResourceServerStoreDelegate().create(clientId);
             registerResourceServerInvalidation(server.getId());
             return server;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index a5823ce..4c28ad6 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -23,6 +23,7 @@ import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.cache.CachedObject;
 import org.keycloak.models.cache.infinispan.entities.CachedClient;
 
 import java.security.MessageDigest;
@@ -36,17 +37,15 @@ import java.util.Set;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class ClientAdapter implements ClientModel {
+public class ClientAdapter implements ClientModel, CachedObject {
     protected RealmCacheSession cacheSession;
     protected RealmModel cachedRealm;
-    protected RealmCache cache;
 
     protected ClientModel updated;
     protected CachedClient cached;
 
-    public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession, RealmCache cache) {
+    public ClientAdapter(RealmModel cachedRealm, CachedClient cached, RealmCacheSession cacheSession) {
         this.cachedRealm = cachedRealm;
-        this.cache = cache;
         this.cacheSession = cacheSession;
         this.cached = cached;
     }
@@ -54,7 +53,7 @@ public class ClientAdapter implements ClientModel {
     private void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerClientInvalidation(cached.getId(), cached.getClientId(), cachedRealm.getId());
-            updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
+            updated = cacheSession.getRealmDelegate().getClientById(cached.getId(), cachedRealm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -66,12 +65,17 @@ public class ClientAdapter implements ClientModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getClientById(cached.getId(), cachedRealm);
+        updated = cacheSession.getRealmDelegate().getClientById(cached.getId(), cachedRealm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
 
     @Override
+    public long getCacheTimestamp() {
+        return cached.getCacheTimestamp();
+    }
+
+    @Override
     public void updateClient() {
         if (updated != null) updated.updateClient();
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
index a521ef2..4d2ce42 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientTemplateAdapter.java
@@ -50,7 +50,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel {
     private void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerClientTemplateInvalidation(cached.getId());
-            updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm);
+            updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -63,7 +63,7 @@ public class ClientTemplateAdapter implements ClientTemplateModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getClientTemplateById(cached.getId(), cachedRealm);
+        updated = cacheSession.getRealmDelegate().getClientTemplateById(cached.getId(), cachedRealm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
index 22fef96..8c88df7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/AbstractRevisioned.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.cache.infinispan.entities;
 
 import org.keycloak.common.util.Time;
+import org.keycloak.models.cache.CachedObject;
 
 import java.io.Serializable;
 
@@ -8,7 +9,7 @@ import java.io.Serializable;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class AbstractRevisioned implements Revisioned, Serializable {
+public class AbstractRevisioned implements Revisioned, Serializable, CachedObject {
     private String id;
     private Long revision;
     private final long cacheTimestamp = Time.currentTimeMillis();
@@ -38,6 +39,7 @@ public class AbstractRevisioned implements Revisioned, Serializable {
      *
      * @return
      */
+    @Override
     public long getCacheTimestamp() {
         return cacheTimestamp;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
index 21bcc66..0e69c68 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
@@ -51,7 +51,7 @@ public class GroupAdapter implements GroupModel {
     protected void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerGroupInvalidation(cached.getId());
-            updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm);
+            updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -64,7 +64,7 @@ public class GroupAdapter implements GroupModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getGroupById(cached.getId(), realm);
+        updated = cacheSession.getRealmDelegate().getGroupById(cached.getId(), realm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index bfa00e0..dd62377 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -24,6 +24,7 @@ import org.keycloak.models.*;
 import org.keycloak.models.cache.CachedRealmModel;
 import org.keycloak.models.cache.infinispan.entities.CachedRealm;
 import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -36,7 +37,6 @@ public class RealmAdapter implements CachedRealmModel {
     protected CachedRealm cached;
     protected RealmCacheSession cacheSession;
     protected volatile RealmModel updated;
-    protected RealmCache cache;
     protected KeycloakSession session;
 
     public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) {
@@ -49,7 +49,7 @@ public class RealmAdapter implements CachedRealmModel {
     public RealmModel getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerRealmInvalidation(cached.getId(), cached.getName());
-            updated = cacheSession.getDelegate().getRealm(cached.getId());
+            updated = cacheSession.getRealmDelegate().getRealm(cached.getId());
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
         return updated;
@@ -81,7 +81,7 @@ public class RealmAdapter implements CachedRealmModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getRealm(cached.getId());
+        updated = cacheSession.getRealmDelegate().getRealm(cached.getId());
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
@@ -1323,35 +1323,43 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public ComponentModel addComponentModel(ComponentModel model) {
         getDelegateForUpdate();
-        evictUsers(model);
+        executeEvictions(model);
         return updated.addComponentModel(model);
     }
 
     @Override
     public ComponentModel importComponentModel(ComponentModel model) {
         getDelegateForUpdate();
-        evictUsers(model);
+        executeEvictions(model);
         return updated.importComponentModel(model);
     }
 
-    public void evictUsers(ComponentModel model) {
-        String parentId = model.getParentId();
-        evictUsers(parentId);
-    }
-
-    public void evictUsers(String parentId) {
-        if (parentId != null && !parentId.equals(getId())) {
-            ComponentModel parent = getComponent(parentId);
+    public void executeEvictions(ComponentModel model) {
+        if (model == null) return;
+        // If not realm component, check to see if it is a user storage provider child component (i.e. LDAP mapper)
+        if (model.getParentId() != null && !model.getParentId().equals(getId())) {
+            ComponentModel parent = getComponent(model.getParentId());
             if (parent != null && UserStorageProvider.class.getName().equals(parent.getProviderType())) {
                 session.userCache().evict(this);
             }
+            return;
+        }
+
+        // invalidate entire user cache if we're dealing with user storage SPI
+        if (UserStorageProvider.class.getName().equals(model.getProviderType())) {
+            session.userCache().evict(this);
+        }
+        // invalidate entire realm if we're dealing with client storage SPI
+        // entire realm because of client roles, client lists, and clients
+        if (ClientStorageProvider.class.getName().equals(model.getProviderType())) {
+            cacheSession.evictRealmOnRemoval(this);
         }
     }
 
     @Override
     public void updateComponent(ComponentModel component) {
         getDelegateForUpdate();
-        evictUsers(component);
+        executeEvictions(component);
         updated.updateComponent(component);
 
     }
@@ -1359,7 +1367,7 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void removeComponent(ComponentModel component) {
         getDelegateForUpdate();
-        evictUsers(component);
+        executeEvictions(component);
         updated.removeComponent(component);
 
     }
@@ -1367,7 +1375,6 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void removeComponents(String parentId) {
         getDelegateForUpdate();
-        evictUsers(parentId);
         updated.removeComponents(parentId);
 
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 83bafb1..018213f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.*;
 import org.keycloak.models.cache.CacheRealmProvider;
@@ -26,6 +27,9 @@ import org.keycloak.models.cache.CachedRealmModel;
 import org.keycloak.models.cache.infinispan.entities.*;
 import org.keycloak.models.cache.infinispan.events.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.CacheableStorageProviderModel;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.client.ClientStorageProviderModel;
 
 import java.util.*;
 
@@ -94,12 +98,13 @@ public class RealmCacheSession implements CacheRealmProvider {
     public static final String ROLES_QUERY_SUFFIX = ".roles";
     protected RealmCacheManager cache;
     protected KeycloakSession session;
-    protected RealmProvider delegate;
+    protected RealmProvider realmDelegate;
+    protected ClientProvider clientDelegate;
     protected boolean transactionActive;
     protected boolean setRollbackOnly;
 
     protected Map<String, RealmAdapter> managedRealms = new HashMap<>();
-    protected Map<String, ClientAdapter> managedApplications = new HashMap<>();
+    protected Map<String, ClientModel> managedApplications = new HashMap<>();
     protected Map<String, ClientTemplateAdapter> managedClientTemplates = new HashMap<>();
     protected Map<String, RoleAdapter> managedRoles = new HashMap<>();
     protected Map<String, GroupAdapter> managedGroups = new HashMap<>();
@@ -134,16 +139,25 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public MigrationModel getMigrationModel() {
-        return getDelegate().getMigrationModel();
+        return getRealmDelegate().getMigrationModel();
     }
 
     @Override
-    public RealmProvider getDelegate() {
+    public RealmProvider getRealmDelegate() {
         if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
-        if (delegate != null) return delegate;
-        delegate = session.getProvider(RealmProvider.class);
-        return delegate;
+        if (realmDelegate != null) return realmDelegate;
+        realmDelegate = session.realmLocalStorage();
+        return realmDelegate;
     }
+    public ClientProvider getClientDelegate() {
+        if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
+        if (clientDelegate != null) return clientDelegate;
+        clientDelegate = session.clientStorageManager();
+        return clientDelegate;
+    }
+
+
+
 
     @Override
     public void registerRealmInvalidation(String id, String name) {
@@ -163,8 +177,8 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     private void invalidateClient(String id) {
         invalidations.add(id);
-        ClientAdapter adapter = managedApplications.get(id);
-        if (adapter != null) adapter.invalidate();
+        ClientModel adapter = managedApplications.get(id);
+        if (adapter != null && adapter instanceof ClientAdapter) ((ClientAdapter)adapter).invalidate();
     }
 
     @Override
@@ -194,9 +208,9 @@ public class RealmCacheSession implements CacheRealmProvider {
         invalidations.addAll(newInvalidations);
         // need to make sure that scope and group mapping clients and groups are invalidated
         for (String id : newInvalidations) {
-            ClientAdapter adapter = managedApplications.get(id);
-            if (adapter != null) {
-                adapter.invalidate();
+            ClientModel adapter = managedApplications.get(id);
+            if (adapter != null && adapter instanceof ClientAdapter){
+                ((ClientAdapter)adapter).invalidate();
                 continue;
             }
             GroupAdapter group = managedGroups.get(id);
@@ -319,7 +333,6 @@ public class RealmCacheSession implements CacheRealmProvider {
             @Override
             public void commit() {
                 try {
-                    if (delegate == null) return;
                     if (clearAll) {
                         cache.clear();
                     }
@@ -360,14 +373,14 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RealmModel createRealm(String name) {
-        RealmModel realm = getDelegate().createRealm(name);
+        RealmModel realm = getRealmDelegate().createRealm(name);
         registerRealmInvalidation(realm.getId(), realm.getName());
         return realm;
     }
 
     @Override
     public RealmModel createRealm(String id, String name) {
-        RealmModel realm =  getDelegate().createRealm(id, name);
+        RealmModel realm =  getRealmDelegate().createRealm(id, name);
         registerRealmInvalidation(realm.getId(), realm.getName());
         return realm;
     }
@@ -381,14 +394,14 @@ public class RealmCacheSession implements CacheRealmProvider {
         boolean wasCached = false;
         if (cached == null) {
             Long loaded = cache.getCurrentRevision(id);
-            RealmModel model = getDelegate().getRealm(id);
+            RealmModel model = getRealmDelegate().getRealm(id);
             if (model == null) return null;
             if (invalidations.contains(id)) return model;
             cached = new CachedRealm(loaded, model);
             cache.addRevisioned(cached, startupRevision);
             wasCached =true;
         } else if (invalidations.contains(id)) {
-            return getDelegate().getRealm(id);
+            return getRealmDelegate().getRealm(id);
         } else if (managedRealms.containsKey(id)) {
             return managedRealms.get(id);
         }
@@ -420,18 +433,18 @@ public class RealmCacheSession implements CacheRealmProvider {
         }
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            RealmModel model = getDelegate().getRealmByName(name);
+            RealmModel model = getRealmDelegate().getRealmByName(name);
             if (model == null) return null;
             if (invalidations.contains(model.getId())) return model;
             query = new RealmListQuery(loaded, cacheKey, model.getId());
             cache.addRevisioned(query, startupRevision);
             return model;
         } else if (invalidations.contains(cacheKey)) {
-            return getDelegate().getRealmByName(name);
+            return getRealmDelegate().getRealmByName(name);
         } else {
             String realmId = query.getRealms().iterator().next();
             if (invalidations.contains(realmId)) {
-                return getDelegate().getRealmByName(name);
+                return getRealmDelegate().getRealmByName(name);
             }
             return getRealm(realmId);
         }
@@ -444,7 +457,7 @@ public class RealmCacheSession implements CacheRealmProvider {
     @Override
     public List<RealmModel> getRealms() {
         // Retrieve realms from backend
-        List<RealmModel> backendRealms = getDelegate().getRealms();
+        List<RealmModel> backendRealms = getRealmDelegate().getRealms();
 
         // Return cache delegates to ensure cache invalidated during write operations
         List<RealmModel> cachedRealms = new LinkedList<RealmModel>();
@@ -460,22 +473,26 @@ public class RealmCacheSession implements CacheRealmProvider {
         RealmModel realm = getRealm(id);
         if (realm == null) return false;
 
-        cache.invalidateObject(id);
-        invalidationEvents.add(RealmRemovedEvent.create(id, realm.getName()));
-        cache.realmRemoval(id, realm.getName(), invalidations);
-        return getDelegate().removeRealm(id);
+        evictRealmOnRemoval(realm);
+        return getRealmDelegate().removeRealm(id);
+    }
+
+    public void evictRealmOnRemoval(RealmModel realm) {
+        cache.invalidateObject(realm.getId());
+        invalidationEvents.add(RealmRemovedEvent.create(realm.getId(), realm.getName()));
+        cache.realmRemoval(realm.getId(), realm.getName(), invalidations);
     }
 
 
     @Override
     public ClientModel addClient(RealmModel realm, String clientId) {
-        ClientModel client = getDelegate().addClient(realm, clientId);
+        ClientModel client = getRealmDelegate().addClient(realm, clientId);
         return addedClient(realm, client);
     }
 
     @Override
     public ClientModel addClient(RealmModel realm, String id, String clientId) {
-        ClientModel client = getDelegate().addClient(realm, id, clientId);
+        ClientModel client = getRealmDelegate().addClient(realm, id, clientId);
         return addedClient(realm, client);
     }
 
@@ -515,7 +532,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getRealmClientsQueryCacheKey(realm.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
         if (queryDB) {
-            return getDelegate().getClients(realm);
+            return getClientDelegate().getClients(realm);
         }
 
         ClientListQuery query = cache.get(cacheKey, ClientListQuery.class);
@@ -525,7 +542,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            List<ClientModel> model = getDelegate().getClients(realm);
+            List<ClientModel> model = getClientDelegate().getClients(realm);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (ClientModel client : model) ids.add(client.getId());
@@ -540,7 +557,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             if (client == null) {
                 // TODO: Handle with cluster invalidations too
                 invalidations.add(cacheKey);
-                return getDelegate().getClients(realm);
+                return getRealmDelegate().getClients(realm);
             }
             list.add(client);
         }
@@ -563,13 +580,14 @@ public class RealmCacheSession implements CacheRealmProvider {
         for (RoleModel role : client.getRoles()) {
             roleRemovalInvalidations(role.getId(), role.getName(), client.getId());
         }
-        return getDelegate().removeClient(id, realm);
+        return getRealmDelegate().removeClient(id, realm);
     }
 
 
     @Override
     public void close() {
-        if (delegate != null) delegate.close();
+        if (realmDelegate != null) realmDelegate.close();
+        if (clientDelegate != null) clientDelegate.close();
     }
 
     @Override
@@ -579,7 +597,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RoleModel addRealmRole(RealmModel realm, String id, String name) {
-        RoleModel role = getDelegate().addRealmRole(realm, id, name);
+        RoleModel role = getRealmDelegate().addRealmRole(realm, id, name);
         addedRole(role.getId(), realm.getId());
         return role;
     }
@@ -589,7 +607,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getRolesCacheKey(realm.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
         if (queryDB) {
-            return getDelegate().getRealmRoles(realm);
+            return getRealmDelegate().getRealmRoles(realm);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -599,7 +617,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            Set<RoleModel> model = getDelegate().getRealmRoles(realm);
+            Set<RoleModel> model = getRealmDelegate().getRealmRoles(realm);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (RoleModel role : model) ids.add(role.getId());
@@ -613,7 +631,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             RoleModel role = session.realms().getRoleById(id, realm);
             if (role == null) {
                 invalidations.add(cacheKey);
-                return getDelegate().getRealmRoles(realm);
+                return getRealmDelegate().getRealmRoles(realm);
             }
             list.add(role);
         }
@@ -625,7 +643,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getRolesCacheKey(client.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(client.getId());
         if (queryDB) {
-            return getDelegate().getClientRoles(realm, client);
+            return getRealmDelegate().getClientRoles(realm, client);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -635,7 +653,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            Set<RoleModel> model = getDelegate().getClientRoles(realm, client);
+            Set<RoleModel> model = getRealmDelegate().getClientRoles(realm, client);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (RoleModel role : model) ids.add(role.getId());
@@ -649,7 +667,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             RoleModel role = session.realms().getRoleById(id, realm);
             if (role == null) {
                 invalidations.add(cacheKey);
-                return getDelegate().getClientRoles(realm, client);
+                return getRealmDelegate().getClientRoles(realm, client);
             }
             list.add(role);
         }
@@ -663,7 +681,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
-        RoleModel role = getDelegate().addClientRole(realm, client, id, name);
+        RoleModel role = getRealmDelegate().addClientRole(realm, client, id, name);
         addedRole(role.getId(), client.getId());
         return role;
     }
@@ -673,7 +691,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getRoleByNameCacheKey(realm.getId(), name);
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
         if (queryDB) {
-            return getDelegate().getRealmRole(realm, name);
+            return getRealmDelegate().getRealmRole(realm, name);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -683,7 +701,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            RoleModel model = getDelegate().getRealmRole(realm, name);
+            RoleModel model = getRealmDelegate().getRealmRole(realm, name);
             if (model == null) return null;
             query = new RoleListQuery(loaded, cacheKey, realm, model.getId());
             logger.tracev("adding realm role cache miss: client {0} key {1}", realm.getName(), cacheKey);
@@ -693,7 +711,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         RoleModel role = getRoleById(query.getRoles().iterator().next(), realm);
         if (role == null) {
             invalidations.add(cacheKey);
-            return getDelegate().getRealmRole(realm, name);
+            return getRealmDelegate().getRealmRole(realm, name);
         }
         return role;
     }
@@ -703,7 +721,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getRoleByNameCacheKey(client.getId(), name);
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(client.getId());
         if (queryDB) {
-            return getDelegate().getClientRole(realm, client, name);
+            return getRealmDelegate().getClientRole(realm, client, name);
         }
 
         RoleListQuery query = cache.get(cacheKey, RoleListQuery.class);
@@ -713,7 +731,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            RoleModel model = getDelegate().getClientRole(realm, client, name);
+            RoleModel model = getRealmDelegate().getClientRole(realm, client, name);
             if (model == null) return null;
             query = new RoleListQuery(loaded, cacheKey, realm, model.getId(), client.getClientId());
             logger.tracev("adding client role cache miss: client {0} key {1}", client.getClientId(), cacheKey);
@@ -723,7 +741,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         RoleModel role = getRoleById(query.getRoles().iterator().next(), realm);
         if (role == null) {
             invalidations.add(cacheKey);
-            return getDelegate().getClientRole(realm, client, name);
+            return getRealmDelegate().getClientRole(realm, client, name);
         }
         return role;
     }
@@ -736,7 +754,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
         roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());
 
-        return getDelegate().removeRole(realm, role);
+        return getRealmDelegate().removeRole(realm, role);
     }
 
     @Override
@@ -748,7 +766,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (cached == null) {
             Long loaded = cache.getCurrentRevision(id);
-            RoleModel model = getDelegate().getRoleById(id, realm);
+            RoleModel model = getRealmDelegate().getRoleById(id, realm);
             if (model == null) return null;
             if (invalidations.contains(id)) return model;
             if (model.isClientRole()) {
@@ -759,7 +777,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             cache.addRevisioned(cached, startupRevision);
 
         } else if (invalidations.contains(id)) {
-            return getDelegate().getRoleById(id, realm);
+            return getRealmDelegate().getRoleById(id, realm);
         } else if (managedRoles.containsKey(id)) {
             return managedRoles.get(id);
         }
@@ -777,14 +795,14 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (cached == null) {
             Long loaded = cache.getCurrentRevision(id);
-            GroupModel model = getDelegate().getGroupById(id, realm);
+            GroupModel model = getRealmDelegate().getGroupById(id, realm);
             if (model == null) return null;
             if (invalidations.contains(id)) return model;
             cached = new CachedGroup(loaded, realm, model);
             cache.addRevisioned(cached, startupRevision);
 
         } else if (invalidations.contains(id)) {
-            return getDelegate().getGroupById(id, realm);
+            return getRealmDelegate().getGroupById(id, realm);
         } else if (managedGroups.containsKey(id)) {
             return managedGroups.get(id);
         }
@@ -800,7 +818,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         listInvalidations.add(realm.getId());
 
         invalidationEvents.add(GroupMovedEvent.create(group, toParent, realm.getId()));
-        getDelegate().moveGroup(realm, group, toParent);
+        getRealmDelegate().moveGroup(realm, group, toParent);
     }
 
     @Override
@@ -808,7 +826,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getGroupsQueryCacheKey(realm.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
         if (queryDB) {
-            return getDelegate().getGroups(realm);
+            return getRealmDelegate().getGroups(realm);
         }
 
         GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
@@ -818,7 +836,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            List<GroupModel> model = getDelegate().getGroups(realm);
+            List<GroupModel> model = getRealmDelegate().getGroups(realm);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (GroupModel client : model) ids.add(client.getId());
@@ -832,7 +850,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             GroupModel group = session.realms().getGroupById(id, realm);
             if (group == null) {
                 invalidations.add(cacheKey);
-                return getDelegate().getGroups(realm);
+                return getRealmDelegate().getGroups(realm);
             }
             list.add(group);
         }
@@ -844,12 +862,12 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups) {
-        return getDelegate().getGroupsCount(realm, onlyTopGroups);
+        return getRealmDelegate().getGroupsCount(realm, onlyTopGroups);
     }
 
     @Override
     public Long getGroupsCountByNameContaining(RealmModel realm, String search) {
-        return getDelegate().getGroupsCountByNameContaining(realm, search);
+        return getRealmDelegate().getGroupsCountByNameContaining(realm, search);
     }
 
     @Override
@@ -857,7 +875,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getTopGroupsQueryCacheKey(realm.getId());
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
         if (queryDB) {
-            return getDelegate().getTopLevelGroups(realm);
+            return getRealmDelegate().getTopLevelGroups(realm);
         }
 
         GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
@@ -867,7 +885,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            List<GroupModel> model = getDelegate().getTopLevelGroups(realm);
+            List<GroupModel> model = getRealmDelegate().getTopLevelGroups(realm);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (GroupModel client : model) ids.add(client.getId());
@@ -881,7 +899,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             GroupModel group = session.realms().getGroupById(id, realm);
             if (group == null) {
                 invalidations.add(cacheKey);
-                return getDelegate().getTopLevelGroups(realm);
+                return getRealmDelegate().getTopLevelGroups(realm);
             }
             list.add(group);
         }
@@ -896,7 +914,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         String cacheKey = getTopGroupsQueryCacheKey(realm.getId() + first + max);
         boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId() + first + max);
         if (queryDB) {
-            return getDelegate().getTopLevelGroups(realm, first, max);
+            return getRealmDelegate().getTopLevelGroups(realm, first, max);
         }
 
         GroupListQuery query = cache.get(cacheKey, GroupListQuery.class);
@@ -906,7 +924,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (Objects.isNull(query)) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            List<GroupModel> model = getDelegate().getTopLevelGroups(realm, first, max);
+            List<GroupModel> model = getRealmDelegate().getTopLevelGroups(realm, first, max);
             if (model == null) return null;
             Set<String> ids = new HashSet<>();
             for (GroupModel client : model) ids.add(client.getId());
@@ -920,7 +938,7 @@ public class RealmCacheSession implements CacheRealmProvider {
             GroupModel group = session.realms().getGroupById(id, realm);
             if (Objects.isNull(group)) {
                 invalidations.add(cacheKey);
-                return getDelegate().getTopLevelGroups(realm);
+                return getRealmDelegate().getTopLevelGroups(realm);
             }
             list.add(group);
         }
@@ -932,7 +950,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public List<GroupModel> searchForGroupByName(RealmModel realm, String search, Integer first, Integer max) {
-        return getDelegate().searchForGroupByName(realm, search, first, max);
+        return getRealmDelegate().searchForGroupByName(realm, search, first, max);
     }
 
     @Override
@@ -946,12 +964,12 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         invalidationEvents.add(GroupRemovedEvent.create(group, realm.getId()));
 
-        return getDelegate().removeGroup(realm, group);
+        return getRealmDelegate().removeGroup(realm, group);
     }
 
     @Override
     public GroupModel createGroup(RealmModel realm, String name) {
-        GroupModel group = getDelegate().createGroup(realm, name);
+        GroupModel group = getRealmDelegate().createGroup(realm, name);
         return groupAdded(realm, group);
     }
 
@@ -965,7 +983,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
     @Override
     public GroupModel createGroup(RealmModel realm, String id, String name) {
-        GroupModel group = getDelegate().createGroup(realm, id, name);
+        GroupModel group = getRealmDelegate().createGroup(realm, id, name);
         return groupAdded(realm, group);
     }
 
@@ -978,7 +996,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         addGroupEventIfAbsent(GroupMovedEvent.create(subGroup, null, realm.getId()));
 
-        getDelegate().addTopLevelGroup(realm, subGroup);
+        getRealmDelegate().addTopLevelGroup(realm, subGroup);
 
     }
 
@@ -1007,22 +1025,80 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (cached == null) {
             Long loaded = cache.getCurrentRevision(id);
-            ClientModel model = getDelegate().getClientById(id, realm);
+            ClientModel model = getClientDelegate().getClientById(id, realm);
             if (model == null) return null;
-            if (invalidations.contains(id)) return model;
-            cached = new CachedClient(loaded, realm, model);
-            logger.tracev("adding client by id cache miss: {0}", cached.getClientId());
-            cache.addRevisioned(cached, startupRevision);
+            ClientModel adapter = cacheClient(realm, model, loaded);
+            managedApplications.put(id, adapter);
+            return adapter;
         } else if (invalidations.contains(id)) {
-            return getDelegate().getClientById(id, realm);
+            return getRealmDelegate().getClientById(id, realm);
         } else if (managedApplications.containsKey(id)) {
             return managedApplications.get(id);
         }
-        ClientAdapter adapter = new ClientAdapter(realm, cached, this, null);
+        ClientModel adapter = validateCache(realm, cached);
         managedApplications.put(id, adapter);
         return adapter;
     }
 
+    protected ClientModel cacheClient(RealmModel realm, ClientModel delegate, Long revision) {
+        if (invalidations.contains(delegate.getId())) return delegate;
+        StorageId storageId = new StorageId(delegate.getId());
+        CachedClient cached = null;
+        ClientAdapter adapter = null;
+
+        if (!storageId.isLocal()) {
+            ComponentModel component = realm.getComponent(storageId.getProviderId());
+            ClientStorageProviderModel model = new ClientStorageProviderModel(component);
+            if (!model.isEnabled()) {
+                return delegate;
+            }
+            ClientStorageProviderModel.CachePolicy policy = model.getCachePolicy();
+            if (policy != null && policy == ClientStorageProviderModel.CachePolicy.NO_CACHE) {
+                return delegate;
+            }
+
+            cached = new CachedClient(revision, realm, delegate);
+            adapter = new ClientAdapter(realm, cached, this);
+
+            long lifespan = model.getLifespan();
+            if (lifespan > 0) {
+                cache.addRevisioned(cached, startupRevision, lifespan);
+            } else {
+                cache.addRevisioned(cached, startupRevision);
+            }
+        } else {
+            cached = new CachedClient(revision, realm, delegate);
+            adapter = new ClientAdapter(realm, cached, this);
+            cache.addRevisioned(cached, startupRevision);
+        }
+
+        return adapter;
+    }
+
+
+    protected ClientModel validateCache(RealmModel realm, CachedClient cached) {
+        if (!realm.getId().equals(cached.getRealm())) {
+            return null;
+        }
+
+        StorageId storageId = new StorageId(cached.getId());
+        if (!storageId.isLocal()) {
+            ComponentModel component = realm.getComponent(storageId.getProviderId());
+            ClientStorageProviderModel model = new ClientStorageProviderModel(component);
+
+            // although we do set a timeout, Infinispan has no guarantees when the user will be evicted
+            // its also hard to test stuff
+            if (model.shouldInvalidate(cached)) {
+                registerClientInvalidation(cached.getId(), cached.getClientId(), realm.getId());
+                return getClientDelegate().getClientById(cached.getId(), realm);
+            }
+        }
+        ClientAdapter adapter = new ClientAdapter(realm, cached, this);
+
+        return adapter;
+    }
+
+
     @Override
     public ClientModel getClientByClientId(String clientId, RealmModel realm) {
         String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
@@ -1035,7 +1111,7 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (query == null) {
             Long loaded = cache.getCurrentRevision(cacheKey);
-            ClientModel model = getDelegate().getClientByClientId(clientId, realm);
+            ClientModel model = getClientDelegate().getClientByClientId(clientId, realm);
             if (model == null) return null;
             if (invalidations.contains(model.getId())) return model;
             id = model.getId();
@@ -1043,11 +1119,11 @@ public class RealmCacheSession implements CacheRealmProvider {
             logger.tracev("adding client by name cache miss: {0}", clientId);
             cache.addRevisioned(query, startupRevision);
         } else if (invalidations.contains(cacheKey)) {
-            return getDelegate().getClientByClientId(clientId, realm);
+            return getClientDelegate().getClientByClientId(clientId, realm);
         } else {
             id = query.getClients().iterator().next();
             if (invalidations.contains(id)) {
-                return getDelegate().getClientByClientId(clientId, realm);
+                return getClientDelegate().getClientByClientId(clientId, realm);
             }
         }
         return getClientById(id, realm);
@@ -1066,13 +1142,13 @@ public class RealmCacheSession implements CacheRealmProvider {
 
         if (cached == null) {
             Long loaded = cache.getCurrentRevision(id);
-            ClientTemplateModel model = getDelegate().getClientTemplateById(id, realm);
+            ClientTemplateModel model = getRealmDelegate().getClientTemplateById(id, realm);
             if (model == null) return null;
             if (invalidations.contains(id)) return model;
             cached = new CachedClientTemplate(loaded, realm, model);
             cache.addRevisioned(cached, startupRevision);
         } else if (invalidations.contains(id)) {
-            return getDelegate().getClientTemplateById(id, realm);
+            return getRealmDelegate().getClientTemplateById(id, realm);
         } else if (managedClientTemplates.containsKey(id)) {
             return managedClientTemplates.get(id);
         }
@@ -1084,31 +1160,31 @@ public class RealmCacheSession implements CacheRealmProvider {
     // Don't cache ClientInitialAccessModel for now
     @Override
     public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
-        return getDelegate().createClientInitialAccessModel(realm, expiration, count);
+        return getRealmDelegate().createClientInitialAccessModel(realm, expiration, count);
     }
 
     @Override
     public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
-        return getDelegate().getClientInitialAccessModel(realm, id);
+        return getRealmDelegate().getClientInitialAccessModel(realm, id);
     }
 
     @Override
     public void removeClientInitialAccessModel(RealmModel realm, String id) {
-        getDelegate().removeClientInitialAccessModel(realm, id);
+        getRealmDelegate().removeClientInitialAccessModel(realm, id);
     }
 
     @Override
     public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
-        return getDelegate().listClientInitialAccess(realm);
+        return getRealmDelegate().listClientInitialAccess(realm);
     }
 
     @Override
     public void removeExpiredClientInitialAccess() {
-        getDelegate().removeExpiredClientInitialAccess();
+        getRealmDelegate().removeExpiredClientInitialAccess();
     }
 
     @Override
     public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
-        getDelegate().decreaseRemainingCount(realm, clientInitialAccess);
+        getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess);
     }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
index 00e41f3..24ed6d9 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java
@@ -49,7 +49,7 @@ public class RoleAdapter implements RoleModel {
     protected void getDelegateForUpdate() {
         if (updated == null) {
             cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());
-            updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
+            updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
     }
@@ -62,7 +62,7 @@ public class RoleAdapter implements RoleModel {
     protected boolean isUpdated() {
         if (updated != null) return true;
         if (!invalidated) return false;
-        updated = cacheSession.getDelegate().getRoleById(cached.getId(), realm);
+        updated = cacheSession.getRealmDelegate().getRoleById(cached.getId(), realm);
         if (updated == null) throw new IllegalStateException("Not found in database");
         return true;
     }
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 4b5309a..be3d0ad 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
@@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
 
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.cache.CachedObject;
 import org.keycloak.models.cache.infinispan.events.InvalidationEvent;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.Time;
@@ -49,9 +50,11 @@ import org.keycloak.models.cache.infinispan.events.UserFederationLinkUpdatedEven
 import org.keycloak.models.cache.infinispan.events.UserFullInvalidationEvent;
 import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent;
 import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
+import org.keycloak.storage.CacheableStorageProviderModel;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProvider;
 
 import java.util.Calendar;
 import java.util.HashMap;
@@ -144,7 +147,6 @@ public class UserCacheSession implements UserCache {
 
             @Override
             public void commit() {
-                if (delegate == null) return;
                 runInvalidations();
                 transactionActive = false;
             }
@@ -296,46 +298,11 @@ public class UserCacheSession implements UserCache {
 
         if (!storageId.isLocal()) {
             ComponentModel component = realm.getComponent(storageId.getProviderId());
-            UserStorageProviderModel model = new UserStorageProviderModel(component);
+            CacheableStorageProviderModel model = new CacheableStorageProviderModel(component);
 
             // although we do set a timeout, Infinispan has no guarantees when the user will be evicted
             // its also hard to test stuff
-            boolean invalidate = false;
-            if (!model.isEnabled()) {
-                invalidate = true;
-            } else {
-                UserStorageProviderModel.CachePolicy policy = model.getCachePolicy();
-                if (policy != null) {
-                    //String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis()));
-                    if (policy == UserStorageProviderModel.CachePolicy.NO_CACHE) {
-                        invalidate = true;
-                    } else if (cached.getCacheTimestamp() < model.getCacheInvalidBefore()) {
-                        invalidate = true;
-                    } else if (policy == UserStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
-                        if (cached.getCacheTimestamp() + model.getMaxLifespan() < Time.currentTimeMillis()) {
-                            invalidate = true;
-                        }
-                    } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) {
-                        long dailyTimeout = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute());
-                        dailyTimeout = dailyTimeout - (24 * 60 * 60 * 1000);
-                        //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(dailyTimeout));
-                        //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
-                        if (cached.getCacheTimestamp() <= dailyTimeout) {
-                            invalidate = true;
-                        }
-                    } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
-                        int oneWeek = 7 * 24 * 60 * 60 * 1000;
-                        long weeklyTimeout = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute());
-                        long lastTimeout = weeklyTimeout - oneWeek;
-                        //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout));
-                        //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
-                        if (cached.getCacheTimestamp() <= lastTimeout) {
-                            invalidate = true;
-                        }
-                    }
-                }
-            }
-            if (invalidate) {
+            if (model.shouldInvalidate(cached)) {
                 registerUserInvalidation(realm, cached);
                 return getDelegate().getUserById(cached.getId(), realm);
             }
@@ -371,26 +338,11 @@ public class UserCacheSession implements UserCache {
             adapter = new UserAdapter(cached, this, session, realm);
             onCache(realm, adapter, delegate);
 
-            if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) {
-                cache.addRevisioned(cached, startupRevision);
+            long lifespan = model.getLifespan();
+            if (lifespan > 0) {
+                cache.addRevisioned(cached, startupRevision, lifespan);
             } else {
-                long lifespan = -1;
-                if (policy == UserStorageProviderModel.CachePolicy.EVICT_DAILY) {
-                    if (model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) {
-                        lifespan = dailyTimeout(model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis();
-                    }
-                } else if (policy == UserStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
-                    if (model.getEvictionDay() > 0 && model.getEvictionHour() > -1 && model.getEvictionMinute() > -1) {
-                        lifespan = weeklyTimeout(model.getEvictionDay(), model.getEvictionHour(), model.getEvictionMinute()) - Time.currentTimeMillis();
-                    }
-                } else if (policy == UserStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
-                    lifespan = model.getMaxLifespan();
-                }
-                if (lifespan > 0) {
-                    cache.addRevisioned(cached, startupRevision, lifespan);
-                } else {
-                    cache.addRevisioned(cached, startupRevision);
-                }
+                cache.addRevisioned(cached, startupRevision);
             }
         } else {
             cached = new CachedUser(revision, realm, delegate, notBefore);
@@ -402,39 +354,6 @@ public class UserCacheSession implements UserCache {
         return adapter;
     }
 
-
-    public static long dailyTimeout(int hour, int minute) {
-        Calendar cal = Calendar.getInstance();
-        Calendar cal2 = Calendar.getInstance();
-        cal.setTimeInMillis(Time.currentTimeMillis());
-        cal2.setTimeInMillis(Time.currentTimeMillis());
-        cal2.set(Calendar.HOUR_OF_DAY, hour);
-        cal2.set(Calendar.MINUTE, minute);
-        if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
-            int add = (24 * 60 * 60 * 1000);
-            cal.add(Calendar.MILLISECOND, add);
-        } else {
-            cal.add(Calendar.MILLISECOND, (int)(cal2.getTimeInMillis() - cal.getTimeInMillis()));
-        }
-        return cal.getTimeInMillis();
-    }
-
-    public static long weeklyTimeout(int day, int hour, int minute) {
-        Calendar cal = Calendar.getInstance();
-        Calendar cal2 = Calendar.getInstance();
-        cal.setTimeInMillis(Time.currentTimeMillis());
-        cal2.setTimeInMillis(Time.currentTimeMillis());
-        cal2.set(Calendar.HOUR_OF_DAY, hour);
-        cal2.set(Calendar.MINUTE, minute);
-        cal2.set(Calendar.DAY_OF_WEEK, day);
-        if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
-            int add = (7 * 24 * 60 * 60 * 1000);
-            cal2.add(Calendar.MILLISECOND, add);
-        }
-
-        return cal2.getTimeInMillis();
-    }
-
     private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) {
         ((OnUserCache)getDelegate()).onCache(realm, adapter, delegate);
         ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate);
@@ -935,7 +854,7 @@ public class UserCacheSession implements UserCache {
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
-        if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
+        if (!component.getProviderType().equals(UserStorageProvider.class.getName()) && !component.getProviderType().equals(ClientStorageProvider.class.getName())) return;
         addRealmInvalidation(realm.getId()); // easier to just invalidate whole realm
         getDelegate().preRemove(realm, component);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 4cfea1d..c0b1aa0 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -20,6 +20,8 @@ package org.keycloak.models.sessions.infinispan;
 import org.infinispan.Cache;
 import org.infinispan.client.hotrod.RemoteCache;
 import org.infinispan.context.Flag;
+import org.infinispan.stream.CacheCollectors;
+import org.infinispan.stream.SerializableSupplier;
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.common.util.Time;
@@ -59,16 +61,21 @@ import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
 import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
 import org.keycloak.models.utils.SessionTimeoutHelper;
 
+import java.io.Serializable;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 /**
@@ -297,16 +304,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
+        final String clientUuid = client.getId();
+        UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).client(clientUuid);
+
+        return getUserSessionModels(realm, firstResult, maxResults, offline, predicate);
+    }
+
+    protected List<UserSessionModel> getUserSessionModels(RealmModel realm, int firstResult, int maxResults, boolean offline, UserSessionPredicate predicate) {
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
         cache = CacheDecorators.skipCacheLoaders(cache);
 
         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = getClientSessionCache(offline);
         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCacheDecorated = CacheDecorators.skipCacheLoaders(clientSessionCache);
 
-        final String clientUuid = client.getId();
-
         Stream<UserSessionEntity> stream = cache.entrySet().stream()
-                .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid))
+                .filter(predicate)
                 .map(Mappers.userSessionEntity())
                 .sorted(Comparators.userSessionLastSessionRefresh());
 
@@ -330,7 +342,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return sessions;
     }
 
-
     @Override
     public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate) {
         UserSessionModel userSession = getUserSession(realm, id, offline);
@@ -398,7 +409,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return getUserSessionsCount(realm, client, false);
     }
 
-    protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
+    @Override
+    public Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline) {
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
+        cache = CacheDecorators.skipCacheLoaders(cache);
+        return cache.entrySet().stream()
+                .filter(UserSessionPredicate.create(realm.getId()))
+                .map(Mappers.authClientSessionSetMapper())
+                .flatMap((Serializable & Function<Set<String>, Stream<? extends String>>)Mappers::toStream)
+                .collect(
+                        CacheCollectors.serializableCollector(
+                                () -> Collectors.groupingBy(Function.identity(), Collectors.counting())
+                        )
+                );
+    }
+
+     protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
         cache = CacheDecorators.skipCacheLoaders(cache);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
index 177fd23..50df448 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models.sessions.infinispan.stream;
 
+import org.infinispan.stream.SerializableSupplier;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
@@ -25,9 +26,15 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
 import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -125,4 +132,22 @@ public class Mappers {
         }
     }
 
+    private static class AuthClientSessionSetMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, Set<String>>, Serializable {
+
+        @Override
+        public Set<String> apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
+            UserSessionEntity entity = entry.getValue().getEntity();
+            return entity.getAuthenticatedClientSessions().keySet();
+        }
+    }
+
+    public static <T> Stream<T> toStream(Collection<T> collection) {
+        return collection.stream();
+    }
+
+    public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, Set<String>> authClientSessionSetMapper() {
+        return new AuthClientSessionSetMapper();
+    }
+
+
 }
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
index 32210a0..d26f2b9 100644
--- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
@@ -20,6 +20,7 @@ package org.keycloak.models.sessions.infinispan.initializer;
 import org.junit.Assert;
 import org.junit.Test;
 import org.keycloak.models.cache.infinispan.UserCacheSession;
+import org.keycloak.storage.CacheableStorageProviderModel;
 
 import java.text.DateFormat;
 import java.util.Calendar;
@@ -65,13 +66,13 @@ public class InitializerStateTest {
 
     @Test
     public void testDailyTimeout() throws Exception {
-        Date date = new Date(UserCacheSession.dailyTimeout(10, 30));
+        Date date = new Date(CacheableStorageProviderModel.dailyTimeout(10, 30));
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
-        date = new Date(UserCacheSession.dailyTimeout(17, 45));
+        date = new Date(CacheableStorageProviderModel.dailyTimeout(17, 45));
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
-        date = new Date(UserCacheSession.weeklyTimeout(Calendar.MONDAY, 13, 45));
+        date = new Date(CacheableStorageProviderModel.weeklyTimeout(Calendar.MONDAY, 13, 45));
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
-        date = new Date(UserCacheSession.weeklyTimeout(Calendar.THURSDAY, 13, 45));
+        date = new Date(CacheableStorageProviderModel.weeklyTimeout(Calendar.THURSDAY, 13, 45));
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
         System.out.println("----");
         Calendar cal = Calendar.getInstance();
@@ -80,7 +81,7 @@ public class InitializerStateTest {
         int min = cal.get(Calendar.MINUTE);
         date = new Date(cal.getTimeInMillis());
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
-        date = new Date(UserCacheSession.dailyTimeout(hour, min));
+        date = new Date(CacheableStorageProviderModel.dailyTimeout(hour, min));
         System.out.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date));
         cal = Calendar.getInstance();
         cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
index 207d4ab..5e79bad 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
@@ -25,6 +25,8 @@ import org.keycloak.authorization.jpa.entities.ScopeEntity;
 import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.models.ModelException;
+import org.keycloak.storage.StorageId;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -46,6 +48,9 @@ public class JPAResourceServerStore implements ResourceServerStore {
 
     @Override
     public ResourceServer create(String clientId) {
+        if (!StorageId.isLocalStorage(clientId)) {
+            throw new ModelException("Creating resource server from federated ClientModel not supported");
+        }
         ResourceServerEntity entity = new ResourceServerEntity();
 
         entity.setId(clientId);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 7f88977..7ede55c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -125,9 +125,6 @@ public class ClientEntity {
     @CollectionTable(name="CLIENT_AUTH_FLOW_BINDINGS", joinColumns={ @JoinColumn(name="CLIENT_ID") })
     protected Map<String, String> authFlowBindings = new HashMap<String, String>();
 
-    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client", cascade = CascadeType.REMOVE)
-    Collection<ClientIdentityProviderMappingEntity> identityProviders = new ArrayList<ClientIdentityProviderMappingEntity>();
-
     @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "client")
     Collection<ProtocolMapperEntity> protocolMappers = new ArrayList<ProtocolMapperEntity>();
 
@@ -322,14 +319,6 @@ public class ClientEntity {
         this.frontchannelLogout = frontchannelLogout;
     }
 
-    public Collection<ClientIdentityProviderMappingEntity> getIdentityProviders() {
-        return this.identityProviders;
-    }
-
-    public void setIdentityProviders(Collection<ClientIdentityProviderMappingEntity> identityProviders) {
-        this.identityProviders = identityProviders;
-    }
-
     public Collection<ProtocolMapperEntity> getProtocolMappers() {
         return protocolMappers;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java
index a29ab69..9772810 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentEntity.java
@@ -43,11 +43,14 @@ import java.util.Collection;
 })
 @NamedQueries({
         @NamedQuery(name="userConsentByUserAndClient", query="select consent from UserConsentEntity consent where consent.user.id = :userId and consent.clientId = :clientId"),
+        @NamedQuery(name="userConsentByUserAndExternalClient", query="select consent from UserConsentEntity consent where consent.user.id = :userId and consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"),
         @NamedQuery(name="userConsentsByUser", query="select consent from UserConsentEntity consent where consent.user.id = :userId"),
         @NamedQuery(name="deleteUserConsentsByRealm", query="delete from UserConsentEntity consent where consent.user IN (select user from UserEntity user where user.realmId = :realmId)"),
         @NamedQuery(name="deleteUserConsentsByRealmAndLink", query="delete from UserConsentEntity consent where consent.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
         @NamedQuery(name="deleteUserConsentsByUser", query="delete from UserConsentEntity consent where consent.user = :user"),
         @NamedQuery(name="deleteUserConsentsByClient", query="delete from UserConsentEntity consent where consent.clientId = :clientId"),
+        @NamedQuery(name="deleteUserConsentsByExternalClient", query="delete from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"),
+        @NamedQuery(name="deleteUserConsentsByClientStorageProvider", query="delete from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider"),
 })
 public class UserConsentEntity {
 
@@ -63,6 +66,12 @@ public class UserConsentEntity {
     @Column(name="CLIENT_ID")
     protected String clientId;
 
+    @Column(name="CLIENT_STORAGE_PROVIDER")
+    protected String clientStorageProvider;
+
+    @Column(name="EXTERNAL_CLIENT_ID")
+    protected String externalClientId;
+
     @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "userConsent")
     Collection<UserConsentRoleEntity> grantedRoles = new ArrayList<UserConsentRoleEntity>();
 
@@ -91,14 +100,6 @@ public class UserConsentEntity {
         this.user = user;
     }
 
-    public String getClientId() {
-        return clientId;
-    }
-
-    public void setClientId(String clientId) {
-        this.clientId = clientId;
-    }
-
     public Collection<UserConsentRoleEntity> getGrantedRoles() {
         return grantedRoles;
     }
@@ -131,6 +132,30 @@ public class UserConsentEntity {
         this.lastUpdatedDate = lastUpdatedDate;
     }
 
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientStorageProvider() {
+        return clientStorageProvider;
+    }
+
+    public void setClientStorageProvider(String clientStorageProvider) {
+        this.clientStorageProvider = clientStorageProvider;
+    }
+
+    public String getExternalClientId() {
+        return externalClientId;
+    }
+
+    public void setExternalClientId(String externalClientId) {
+        this.externalClientId = externalClientId;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java
index 4c0dd5d..85df759 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentProtocolMapperEntity.java
@@ -38,7 +38,9 @@ import java.io.Serializable;
         @NamedQuery(name="deleteUserConsentProtMappersByUser", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.user = :user)"),
         @NamedQuery(name="deleteUserConsentProtMappersByRealmAndLink", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link))"),
         @NamedQuery(name="deleteUserConsentProtMappersByProtocolMapper", query="delete from UserConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId)"),
-        @NamedQuery(name="deleteUserConsentProtMappersByClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId))"),
+        @NamedQuery(name="deleteUserConsentProtMappersByClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId)"),
+        @NamedQuery(name="deleteUserConsentProtMappersByExternalClient", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"),
+        @NamedQuery(name="deleteUserConsentProtMappersByClientStorageProvider", query="delete from UserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"),
 })
 @Entity
 @Table(name="USER_CONSENT_PROT_MAPPER")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java
index 95d5f3e..c4818c7 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserConsentRoleEntity.java
@@ -38,6 +38,8 @@ import java.io.Serializable;
         @NamedQuery(name="deleteUserConsentRolesByUser", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.user = :user)"),
         @NamedQuery(name="deleteUserConsentRolesByRole", query="delete from UserConsentRoleEntity grantedRole where grantedRole.roleId = :roleId)"),
         @NamedQuery(name="deleteUserConsentRolesByClient", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientId = :clientId)"),
+        @NamedQuery(name="deleteUserConsentRolesByExternalClient", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"),
+        @NamedQuery(name="deleteUserConsentRolesByClientStorageProvider", query="delete from UserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from UserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"),
 })
 @Entity
 @Table(name="USER_CONSENT_ROLE")
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 08f3164..8eb8102 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
@@ -45,7 +45,9 @@ import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.utils.DefaultRoles;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -194,7 +196,14 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
         consentEntity = new UserConsentEntity();
         consentEntity.setId(KeycloakModelUtils.generateId());
         consentEntity.setUser(em.getReference(UserEntity.class, userId));
-        consentEntity.setClientId(clientId);
+        StorageId clientStorageId = new StorageId(clientId);
+        if (clientStorageId.isLocal()) {
+            consentEntity.setClientId(clientId);
+        } else {
+            consentEntity.setClientStorageProvider(clientStorageId.getProviderId());
+            consentEntity.setExternalClientId(clientStorageId.getExternalId());
+        }
+
         consentEntity.setCreatedDate(currentTime);
         consentEntity.setLastUpdatedDate(currentTime);
         em.persist(consentEntity);
@@ -246,9 +255,16 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
 
     private UserConsentEntity getGrantedConsentEntity(String userId, String clientId) {
-        TypedQuery<UserConsentEntity> query = em.createNamedQuery("userConsentByUserAndClient", UserConsentEntity.class);
+        StorageId clientStorageId = new StorageId(clientId);
+        String queryName = clientStorageId.isLocal() ?  "userConsentByUserAndClient" : "userConsentByUserAndExternalClient";
+        TypedQuery<UserConsentEntity> query = em.createNamedQuery(queryName, UserConsentEntity.class);
         query.setParameter("userId", userId);
-        query.setParameter("clientId", clientId);
+        if (clientStorageId.isLocal()) {
+            query.setParameter("clientId", clientId);
+        } else {
+            query.setParameter("clientStorageProvider", clientStorageId.getProviderId());
+            query.setParameter("externalClientId", clientStorageId.getExternalId());
+        }
         List<UserConsentEntity> results = query.getResultList();
         if (results.size() > 1) {
             throw new ModelException("More results found for user [" + userId + "] and client [" + clientId + "]");
@@ -257,6 +273,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
         } else {
             return null;
         }
+
     }
 
     private UserConsentModel toConsentModel(RealmModel realm, UserConsentEntity entity) {
@@ -264,9 +281,16 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
             return null;
         }
 
-        ClientModel client = realm.getClientById(entity.getClientId());
+        StorageId clientStorageId = null;
+        if ( entity.getClientId() == null) {
+            clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId());
+        } else {
+            clientStorageId = new StorageId(entity.getClientId());
+        }
+
+        ClientModel client = realm.getClientById(clientStorageId.getId());
         if (client == null) {
-            throw new ModelException("Client with id " + entity.getClientId() + " is not available");
+            throw new ModelException("Client with id " + clientStorageId.getId() + " is not available");
         }
         UserConsentModel model = new UserConsentModel(client);
         model.setCreatedDate(entity.getCreatedDate());
@@ -472,9 +496,32 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
     @Override
     public void preRemove(RealmModel realm, ClientModel client) {
-        em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
-        em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
-        em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+        StorageId clientStorageId = new StorageId(client.getId());
+        if (clientStorageId.isLocal()) {
+            em.createNamedQuery("deleteUserConsentProtMappersByClient")
+                    .setParameter("clientId", client.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteUserConsentRolesByClient")
+                    .setParameter("clientId", client.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteUserConsentsByClient")
+                    .setParameter("clientId", client.getId())
+                    .executeUpdate();
+        } else {
+            em.createNamedQuery("deleteUserConsentProtMappersByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId",clientStorageId.getExternalId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteUserConsentRolesByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId", clientStorageId.getExternalId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteUserConsentsByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId", clientStorageId.getExternalId())
+                    .executeUpdate();
+
+        }
     }
 
     @Override
@@ -806,8 +853,24 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
-        if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
-        removeImportedUsers(realm, component.getId());
+        if (component.getProviderType().equals(UserStorageProvider.class.getName())) {
+            removeImportedUsers(realm, component.getId());
+        }
+        if (component.getProviderType().equals(ClientStorageProvider.class.getName())) {
+            removeConsentByClientStorageProvider(realm, component.getId());
+        }
+    }
+
+    protected void removeConsentByClientStorageProvider(RealmModel realm, String providerId) {
+        em.createNamedQuery("deleteUserConsentProtMappersByClientStorageProvider")
+                .setParameter("clientStorageProvider", providerId)
+                .executeUpdate();
+        em.createNamedQuery("deleteUserConsentRolesByClientStorageProvider")
+                .setParameter("clientStorageProvider", providerId)
+                .executeUpdate();
+        em.createNamedQuery("deleteUserConsentsByClientStorageProvider")
+                .setParameter("clientStorageProvider", providerId)
+                .executeUpdate();
 
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
index a4c02de..587ed7e 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
@@ -30,6 +30,7 @@ import org.keycloak.models.session.PersistentClientSessionModel;
 import org.keycloak.models.session.PersistentUserSessionAdapter;
 import org.keycloak.models.session.PersistentUserSessionModel;
 import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.storage.StorageId;
 
 import javax.persistence.EntityManager;
 import javax.persistence.Query;
@@ -78,7 +79,17 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
         PersistentClientSessionModel model = adapter.getUpdatedModel();
 
         PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
-        entity.setClientId(clientSession.getClient().getId());
+        StorageId clientStorageId = new StorageId(clientSession.getClient().getId());
+        if (clientStorageId.isLocal()) {
+            entity.setClientId(clientStorageId.getId());
+            entity.setClientStorageProvider(PersistentClientSessionEntity.LOCAL);
+            entity.setExternalClientId(PersistentClientSessionEntity.LOCAL);
+
+        } else {
+            entity.setClientId(PersistentClientSessionEntity.EXTERNAL);
+            entity.setClientStorageProvider(clientStorageId.getProviderId());
+            entity.setExternalClientId(clientStorageId.getExternalId());
+        }
         entity.setTimestamp(clientSession.getTimestamp());
         String offlineStr = offlineToString(offline);
         entity.setOffline(offlineStr);
@@ -127,7 +138,18 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
     @Override
     public void removeClientSession(String userSessionId, String clientUUID, boolean offline) {
         String offlineStr = offlineToString(offline);
-        PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientUUID, offlineStr));
+        StorageId clientStorageId = new StorageId(clientUUID);
+        String clientId = PersistentClientSessionEntity.EXTERNAL;
+        String clientStorageProvider = PersistentClientSessionEntity.LOCAL;
+        String externalId = PersistentClientSessionEntity.LOCAL;
+        if (clientStorageId.isLocal()) {
+            clientId = clientUUID;
+        } else {
+            clientStorageProvider = clientStorageId.getProviderId();
+            externalId = clientStorageId.getExternalId();
+
+        }
+        PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientId, clientStorageProvider, externalId, offlineStr));
         if (sessionEntity != null) {
             em.remove(sessionEntity);
 
@@ -168,7 +190,16 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
     }
 
     private void onClientRemoved(String clientUUID) {
-        int num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", clientUUID).executeUpdate();
+        int num = 0;
+        StorageId clientStorageId = new StorageId(clientUUID);
+        if (clientStorageId.isLocal()) {
+            num = em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", clientUUID).executeUpdate();
+        } else {
+            num = em.createNamedQuery("deleteClientSessionsByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId", clientStorageId.getExternalId())
+                    .executeUpdate();
+        }
         num = em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
     }
 
@@ -282,10 +313,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
     }
 
     private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
-        ClientModel client = realm.getClientById(entity.getClientId());
+        String clientId = entity.getClientId();
+        if (!entity.getExternalClientId().equals("local")) {
+            clientId = new StorageId(entity.getClientId(), entity.getExternalClientId()).getId();
+        }
+        ClientModel client = realm.getClientById(clientId);
 
         PersistentClientSessionModel model = new PersistentClientSessionModel();
-        model.setClientId(entity.getClientId());
+        model.setClientId(clientId);
         model.setUserSessionId(userSession.getId());
         model.setUserId(userSession.getUserId());
         model.setTimestamp(entity.getTimestamp());
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
index 3ae17b2..44c3188 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
@@ -32,6 +32,8 @@ import java.io.Serializable;
 @NamedQueries({
         @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId = :realmId)"),
         @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId = :clientId"),
+        @NamedQuery(name="deleteClientSessionsByExternalClient", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider and sess.externalClientId = :externalClientId"),
+        @NamedQuery(name="deleteClientSessionsByClientStorageProvider", query="delete from PersistentClientSessionEntity sess where sess.clientStorageProvider = :clientStorageProvider"),
         @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId = :userId)"),
         @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId = :userSessionId and sess.offline = :offline"),
         @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where NOT EXISTS (select u.userSessionId from PersistentUserSessionEntity u where u.userSessionId = sess.userSessionId )"),
@@ -44,6 +46,8 @@ import java.io.Serializable;
 @IdClass(PersistentClientSessionEntity.Key.class)
 public class PersistentClientSessionEntity {
 
+    public static final String LOCAL = "local";
+    public static final String EXTERNAL = "external";
     @Id
     @Column(name = "USER_SESSION_ID", length = 36)
     protected String userSessionId;
@@ -52,6 +56,14 @@ public class PersistentClientSessionEntity {
     @Column(name="CLIENT_ID", length = 36)
     protected String clientId;
 
+    @Id
+    @Column(name="CLIENT_STORAGE_PROVIDER", length = 36)
+    protected String clientStorageProvider;
+
+    @Id
+    @Column(name="EXTERNAL_CLIENT_ID", length = 255)
+    protected String externalClientId;
+
     @Column(name="TIMESTAMP")
     protected int timestamp;
 
@@ -78,6 +90,22 @@ public class PersistentClientSessionEntity {
         this.clientId = clientId;
     }
 
+    public String getClientStorageProvider() {
+        return clientStorageProvider;
+    }
+
+    public void setClientStorageProvider(String clientStorageProvider) {
+        this.clientStorageProvider = clientStorageProvider;
+    }
+
+    public String getExternalClientId() {
+        return externalClientId;
+    }
+
+    public void setExternalClientId(String externalClientId) {
+        this.externalClientId = externalClientId;
+    }
+
     public int getTimestamp() {
         return timestamp;
     }
@@ -107,15 +135,19 @@ public class PersistentClientSessionEntity {
         protected String userSessionId;
 
         protected String clientId;
+        protected String clientStorageProvider;
+        protected String externalClientId;
 
         protected String offline;
 
         public Key() {
         }
 
-        public Key(String userSessionId, String clientId, String offline) {
+        public Key(String userSessionId, String clientId, String clientStorageProvider, String externalClientId, String offline) {
             this.userSessionId = userSessionId;
             this.clientId = clientId;
+            this.externalClientId = externalClientId;
+            this.clientStorageProvider = clientStorageProvider;
             this.offline = offline;
         }
 
@@ -131,6 +163,14 @@ public class PersistentClientSessionEntity {
             return offline;
         }
 
+        public String getClientStorageProvider() {
+            return clientStorageProvider;
+        }
+
+        public String getExternalClientId() {
+            return externalClientId;
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
@@ -140,6 +180,8 @@ public class PersistentClientSessionEntity {
 
             if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
             if (this.clientId != null ? !this.clientId.equals(key.clientId) : key.clientId != null) return false;
+            if (this.externalClientId != null ? !this.externalClientId.equals(key.clientId) : key.externalClientId != null) return false;
+            if (this.clientStorageProvider != null ? !this.clientStorageProvider.equals(key.clientId) : key.clientStorageProvider != null) return false;
             if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
 
             return true;
@@ -149,6 +191,8 @@ public class PersistentClientSessionEntity {
         public int hashCode() {
             int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0;
             result = 37 * result + (this.clientId != null ? this.clientId.hashCode() : 0);
+            result = 37 * result + (this.externalClientId != null ? this.externalClientId.hashCode() : 0);
+            result = 37 * result + (this.clientStorageProvider != null ? this.clientStorageProvider.hashCode() : 0);
             result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0);
             return result;
         }
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java
index 225e80b..c2eac0b 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentEntity.java
@@ -40,11 +40,14 @@ import java.util.Collection;
 })
 @NamedQueries({
         @NamedQuery(name="userFederatedConsentByUserAndClient", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId and consent.clientId = :clientId"),
+        @NamedQuery(name="userFederatedConsentByUserAndExternalClient", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId and consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"),
         @NamedQuery(name="userFederatedConsentsByUser", query="select consent from FederatedUserConsentEntity consent where consent.userId = :userId"),
         @NamedQuery(name="deleteFederatedUserConsentsByRealm", query="delete from FederatedUserConsentEntity consent where consent.realmId=:realmId"),
         @NamedQuery(name="deleteFederatedUserConsentsByStorageProvider", query="delete from FederatedUserConsentEntity e where e.storageProviderId=:storageProviderId"),
         @NamedQuery(name="deleteFederatedUserConsentsByUser", query="delete from FederatedUserConsentEntity consent where consent.userId = :userId and consent.realmId = :realmId"),
         @NamedQuery(name="deleteFederatedUserConsentsByClient", query="delete from FederatedUserConsentEntity consent where consent.clientId = :clientId"),
+        @NamedQuery(name="deleteFederatedUserConsentsByExternalClient", query="delete from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId"),
+        @NamedQuery(name="deleteFederatedUserConsentsByClientStorageProvider", query="delete from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider"),
 })
 public class FederatedUserConsentEntity {
 
@@ -65,6 +68,12 @@ public class FederatedUserConsentEntity {
     @Column(name="CLIENT_ID")
     protected String clientId;
 
+    @Column(name="CLIENT_STORAGE_PROVIDER")
+    protected String clientStorageProvider;
+
+    @Column(name="EXTERNAL_CLIENT_ID")
+    protected String externalClientId;
+
     @Column(name = "CREATED_DATE")
     private Long createdDate;
 
@@ -119,6 +128,22 @@ public class FederatedUserConsentEntity {
         this.clientId = clientId;
     }
 
+    public String getClientStorageProvider() {
+        return clientStorageProvider;
+    }
+
+    public void setClientStorageProvider(String clientStorageProvider) {
+        this.clientStorageProvider = clientStorageProvider;
+    }
+
+    public String getExternalClientId() {
+        return externalClientId;
+    }
+
+    public void setExternalClientId(String externalClientId) {
+        this.externalClientId = externalClientId;
+    }
+
     public Collection<FederatedUserConsentRoleEntity> getGrantedRoles() {
         return grantedRoles;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java
index f7da2cc..a9de2c6 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentProtocolMapperEntity.java
@@ -39,6 +39,8 @@ import java.io.Serializable;
         @NamedQuery(name="deleteFederatedUserConsentProtMappersByStorageProvider", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.storageProviderId = :storageProviderId)"),
         @NamedQuery(name="deleteFederatedUserConsentProtMappersByProtocolMapper", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.protocolMapperId = :protocolMapperId"),
         @NamedQuery(name="deleteFederatedUserConsentProtMappersByClient", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientId = :clientId)"),
+        @NamedQuery(name="deleteFederatedUserConsentProtMappersByExternalClient", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"),
+        @NamedQuery(name="deleteFederatedUserConsentProtMappersByClientStorageProvider", query="delete from FederatedUserConsentProtocolMapperEntity csm where csm.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"),
 })
 @Entity
 @Table(name="FED_USER_CONSENT_PROT_MAPPER")
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java
index d74865d..0e0551d 100755
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUserConsentRoleEntity.java
@@ -38,6 +38,8 @@ import java.io.Serializable;
         @NamedQuery(name="deleteFederatedUserConsentRolesByStorageProvider", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.storageProviderId = :storageProviderId)"),
         @NamedQuery(name="deleteFederatedUserConsentRolesByRole", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.roleId = :roleId"),
         @NamedQuery(name="deleteFederatedUserConsentRolesByClient", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientId = :clientId)"),
+        @NamedQuery(name="deleteFederatedUserConsentRolesByExternalClient", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider and consent.externalClientId = :externalClientId)"),
+        @NamedQuery(name="deleteFederatedUserConsentRolesByClientStorageProvider", query="delete from FederatedUserConsentRoleEntity grantedRole where grantedRole.userConsent IN (select consent from FederatedUserConsentEntity consent where consent.clientStorageProvider = :clientStorageProvider)"),
 })
 @Entity
 @Table(name="FED_USER_CONSENT_ROLE")
diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
index f6de431..7474155 100644
--- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java
@@ -32,9 +32,11 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.jpa.entities.UserConsentEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.storage.StorageId;
 import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.storage.jpa.entity.BrokerLinkEntity;
 import org.keycloak.storage.jpa.entity.FederatedUser;
@@ -257,7 +259,13 @@ public class JpaUserFederatedStorageProvider implements
         consentEntity = new FederatedUserConsentEntity();
         consentEntity.setId(KeycloakModelUtils.generateId());
         consentEntity.setUserId(userId);
-        consentEntity.setClientId(clientId);
+        StorageId clientStorageId = new StorageId(clientId);
+        if (clientStorageId.isLocal()) {
+            consentEntity.setClientId(clientId);
+        } else {
+            consentEntity.setClientStorageProvider(clientStorageId.getProviderId());
+            consentEntity.setExternalClientId(clientStorageId.getExternalId());
+        }
         consentEntity.setRealmId(realm.getId());
         consentEntity.setStorageProviderId(new StorageId(userId).getProviderId());
         long currentTime = Time.currentTimeMillis();
@@ -315,9 +323,16 @@ public class JpaUserFederatedStorageProvider implements
     }
 
     private FederatedUserConsentEntity getGrantedConsentEntity(String userId, String clientId) {
-        TypedQuery<FederatedUserConsentEntity> query = em.createNamedQuery("userFederatedConsentByUserAndClient", FederatedUserConsentEntity.class);
+        StorageId clientStorageId = new StorageId(clientId);
+        String queryName = clientStorageId.isLocal() ?  "userFederatedConsentByUserAndClient" : "userFederatedConsentByUserAndExternalClient";
+        TypedQuery<FederatedUserConsentEntity> query = em.createNamedQuery(queryName, FederatedUserConsentEntity.class);
         query.setParameter("userId", userId);
-        query.setParameter("clientId", clientId);
+        if (clientStorageId.isLocal()) {
+            query.setParameter("clientId", clientId);
+        } else {
+            query.setParameter("clientStorageProvider", clientStorageId.getProviderId());
+            query.setParameter("externalClientId", clientStorageId.getExternalId());
+        }
         List<FederatedUserConsentEntity> results = query.getResultList();
         if (results.size() > 1) {
             throw new ModelException("More results found for user [" + userId + "] and client [" + clientId + "]");
@@ -334,10 +349,14 @@ public class JpaUserFederatedStorageProvider implements
             return null;
         }
 
-        ClientModel client = realm.getClientById(entity.getClientId());
-        if (client == null) {
-            throw new ModelException("Client with id " + entity.getClientId() + " is not available");
+        StorageId clientStorageId = null;
+        if ( entity.getClientId() == null) {
+            clientStorageId = new StorageId(entity.getClientStorageProvider(), entity.getExternalClientId());
+        } else {
+            clientStorageId = new StorageId(entity.getClientId());
         }
+
+        ClientModel client = realm.getClientById(clientStorageId.getId());
         UserConsentModel model = new UserConsentModel(client);
         model.setCreatedDate(entity.getCreatedDate());
         model.setLastUpdatedDate(entity.getLastUpdatedDate());
@@ -822,9 +841,26 @@ public class JpaUserFederatedStorageProvider implements
 
     @Override
     public void preRemove(RealmModel realm, ClientModel client) {
-        em.createNamedQuery("deleteFederatedUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
-        em.createNamedQuery("deleteFederatedUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
-        em.createNamedQuery("deleteFederatedUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+        StorageId clientStorageId = new StorageId(client.getId());
+        if (clientStorageId.isLocal()) {
+            em.createNamedQuery("deleteFederatedUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
+        } else {
+            em.createNamedQuery("deleteFederatedUserConsentProtMappersByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId",clientStorageId.getExternalId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentRolesByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId",clientStorageId.getExternalId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentsByExternalClient")
+                    .setParameter("clientStorageProvider", clientStorageId.getProviderId())
+                    .setParameter("externalClientId",clientStorageId.getExternalId())
+                    .executeUpdate();
+
+        }
     }
 
     @Override
@@ -885,41 +921,53 @@ public class JpaUserFederatedStorageProvider implements
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel model) {
-        if (!model.getProviderType().equals(UserStorageProvider.class.getName())) return;
+        if (model.getProviderType().equals(UserStorageProvider.class.getName())) {
+
+            em.createNamedQuery("deleteBrokerLinkByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedAttributesByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentProtMappersByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserGroupMembershipByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserRequiredActionsByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUsersByStorageProvider")
+                    .setParameter("storageProviderId", model.getId())
+                    .executeUpdate();
+        } else if (model.getProviderType().equals(ClientStorageProvider.class.getName())) {
+            em.createNamedQuery("deleteFederatedUserConsentProtMappersByClientStorageProvider")
+                    .setParameter("clientStorageProvider", model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentRolesByClientStorageProvider")
+                    .setParameter("clientStorageProvider",  model.getId())
+                    .executeUpdate();
+            em.createNamedQuery("deleteFederatedUserConsentsByClientStorageProvider")
+                    .setParameter("clientStorageProvider",  model.getId())
+                    .executeUpdate();
 
-        em.createNamedQuery("deleteBrokerLinkByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedAttributesByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserConsentProtMappersByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserGroupMembershipByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserRequiredActionsByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
-        em.createNamedQuery("deleteFederatedUsersByStorageProvider")
-                .setParameter("storageProviderId", model.getId())
-                .executeUpdate();
+        }
 
     }
 }
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
index f7ebc68..2987163 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-4.0.0.xml
@@ -29,4 +29,62 @@
         </createTable>
         <addPrimaryKey columnNames="CLIENT_ID, BINDING_NAME" constraintName="C_CLI_FLOW_BIND" tableName="CLIENT_AUTH_FLOW_BINDINGS"/>
     </changeSet>
+    <changeSet author="bburke@redhat.com" id="4.0.0-CLEANUP-UNUSED-TABLE">
+        <dropIndex tableName="CLIENT_IDENTITY_PROV_MAPPING" indexName="IDX_CLIENT_ID_PROV_MAP_CLIENT"/>
+        <dropPrimaryKey tableName="CLIENT_IDENTITY_PROV_MAPPING" constraintName="CONSTR_CLIENT_IDEN_PROV_MAP"/>
+        <dropUniqueConstraint tableName="CLIENT_IDENTITY_PROV_MAPPING" constraintName="UK_7CAELWNIBJI49AVXSRTUF6XJ12"/>
+        <dropTable tableName="CLIENT_IDENTITY_PROV_MAPPING"/>
+    </changeSet>
+    <changeSet author="bburke@redhat.com" id="4.0.0-KEYCLOAK-6228">
+        <!-- Modifying some columns so that CLIENT_ID is 255.  Drop foreign key constraints too that referenced CLIENT tablename.
+             This is needed for client storage SPI but only needed for tables that might reference a federated client -->
+
+        <!--  Modify USER_CONSENT -->
+        <dropUniqueConstraint constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
+        <dropNotNullConstraint tableName="USER_CONSENT" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+        <addColumn tableName="USER_CONSENT">
+            <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+            <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)">
+                <constraints nullable="true"/>
+            </column>
+        </addColumn>
+        <addUniqueConstraint columnNames="CLIENT_ID, CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT" tableName="USER_CONSENT"/>
+
+        <!-- FED_USER_CONSENT -->
+        <addColumn tableName="FED_USER_CONSENT">
+            <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+            <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)">
+                <constraints nullable="true"/>
+            </column>
+        </addColumn>
+        <dropNotNullConstraint tableName="FED_USER_CONSENT" columnName="CLIENT_ID" columnDataType="VARCHAR(36)"/>
+        <createIndex tableName="FED_USER_CONSENT" indexName="IDX_FU_CNSNT_EXT">
+            <column name="USER_ID" type="VARCHAR(255)" />
+            <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)" />
+            <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)" />
+        </createIndex>
+
+        <!-- Modify OFFLINE_CLIENT_SESSION -->
+        <addColumn tableName="OFFLINE_CLIENT_SESSION">
+            <column name="CLIENT_STORAGE_PROVIDER" type="VARCHAR(36)" defaultValue="local">
+                <constraints nullable="false"/>
+            </column>
+             <column name="EXTERNAL_CLIENT_ID" type="VARCHAR(255)" defaultValue="local">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+        <update tableName="OFFLINE_CLIENT_SESSION">
+            <column name="CLIENT_STORAGE_PROVIDER" value="local"/>
+        </update>
+        <update tableName="OFFLINE_CLIENT_SESSION">
+            <column name="EXTERNAL_CLIENT_ID" value="local"/>
+        </update>
+        <dropPrimaryKey tableName="OFFLINE_CLIENT_SESSION" constraintName="CONSTRAINT_OFFL_CL_SES_PK3"/>
+        <addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
+
+     </changeSet>
 </databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index f23198c..36e3fb4 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -39,7 +39,6 @@
         <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
         <class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
-        <class>org.keycloak.models.jpa.entities.ClientIdentityProviderMappingEntity</class>
         <class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
         <class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
         <class>org.keycloak.models.jpa.entities.UserConsentRoleEntity</class>
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
new file mode 100644
index 0000000..29dc415
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java
@@ -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.
+ */
+package org.keycloak.models;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.storage.client.ClientLookupProvider;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientProvider extends ClientLookupProvider, Provider {
+    List<ClientModel> getClients(RealmModel realm);
+
+    ClientModel addClient(RealmModel realm, String clientId);
+
+    ClientModel addClient(RealmModel realm, String id, String clientId);
+
+    RoleModel addClientRole(RealmModel realm, ClientModel client, String name);
+
+    RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name);
+
+    RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
+
+    Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client);
+
+    boolean removeClient(String id, RealmModel realm);
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
index c239fb2..72a0d4c 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
@@ -124,6 +124,8 @@ public interface KeycloakSession {
     UserProvider users();
 
 
+    ClientProvider clientStorageManager();
+
     /**
      * Un-cached view of all users in system including users loaded by UserStorageProviders
      *
@@ -145,6 +147,15 @@ public interface KeycloakSession {
      */
     UserProvider userLocalStorage();
 
+    RealmProvider realmLocalStorage();
+
+    /**
+     * Keycloak specific local storage for clients.  No cache in front, this api talks directly to database configured for Keycloak
+     *
+     * @return
+     */
+    ClientProvider clientLocalStorage();
+
     /**
      * Hybrid storage for UserStorageProviders that can't store a specific piece of keycloak data in their external storage.
      * No cache in front.
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 6d48425..5eb18db 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -22,6 +22,8 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.provider.ProviderEvent;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.client.ClientStorageProviderModel;
 
 import java.util.*;
 
@@ -341,6 +343,16 @@ public interface RealmModel extends RoleContainerModel {
         return list;
     }
 
+    default
+    List<ClientStorageProviderModel> getClientStorageProviders() {
+        List<ClientStorageProviderModel> list = new LinkedList<>();
+        for (ComponentModel component : getComponents(getId(), ClientStorageProvider.class.getName())) {
+            list.add(new ClientStorageProviderModel(component));
+        }
+        Collections.sort(list, ClientStorageProviderModel.comparator);
+        return list;
+    }
+
     String getLoginTheme();
 
     void setLoginTheme(String name);
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
index d14f2d6..6fed88a 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.models;
 
 import org.keycloak.migration.MigrationModel;
 import org.keycloak.provider.Provider;
+import org.keycloak.storage.client.ClientLookupProvider;
 
 import java.util.List;
 import java.util.Set;
@@ -27,7 +28,7 @@ import java.util.Set;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public interface RealmProvider extends Provider {
+public interface RealmProvider extends Provider, ClientProvider {
 
     // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession
     MigrationModel getMigrationModel();
@@ -58,15 +59,6 @@ public interface RealmProvider extends Provider {
 
     void addTopLevelGroup(RealmModel realm, GroupModel subGroup);
 
-    ClientModel addClient(RealmModel realm, String clientId);
-
-    ClientModel addClient(RealmModel realm, String id, String clientId);
-
-    List<ClientModel> getClients(RealmModel realm);
-
-    ClientModel getClientById(String id, RealmModel realm);
-    ClientModel getClientByClientId(String clientId, RealmModel realm);
-
 
     RoleModel addRealmRole(RealmModel realm, String name);
 
@@ -74,22 +66,12 @@ public interface RealmProvider extends Provider {
 
     RoleModel getRealmRole(RealmModel realm, String name);
 
-    RoleModel addClientRole(RealmModel realm, ClientModel client, String name);
-
-    RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name);
-
     Set<RoleModel> getRealmRoles(RealmModel realm);
 
-    RoleModel getClientRole(RealmModel realm, ClientModel client, String name);
-
-    Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client);
-
     boolean removeRole(RealmModel realm, RoleModel role);
 
     RoleModel getRoleById(String id, RealmModel realm);
 
-    boolean removeClient(String id, RealmModel realm);
-
     ClientTemplateModel getClientTemplateById(String id, RealmModel realm);
     GroupModel getGroupById(String id, RealmModel realm);
 
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index fc67d4e..ac1c224 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.models;
 import org.keycloak.provider.Provider;
 
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.function.Predicate;
 
@@ -49,6 +50,15 @@ public interface UserSessionProvider extends Provider {
 
     long getActiveUserSessions(RealmModel realm, ClientModel client);
 
+    /**
+     * Returns a summary of client sessions key is client.getId()
+     *
+     * @param realm
+     * @param offline
+     * @return
+     */
+    Map<String, Long> getActiveClientSessionStats(RealmModel realm, boolean offline);
+
     /** This will remove attached ClientLoginSessionModels too **/
     void removeUserSession(RealmModel realm, UserSessionModel session);
     void removeUserSessions(RealmModel realm, UserModel user);
diff --git a/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
new file mode 100644
index 0000000..dd740f2
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/CacheableStorageProviderModel.java
@@ -0,0 +1,264 @@
+/*
+ * 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.storage;
+
+import org.keycloak.common.util.Time;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.PrioritizedComponentModel;
+import org.keycloak.models.cache.CachedObject;
+
+import java.util.Calendar;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class CacheableStorageProviderModel extends PrioritizedComponentModel {
+    public static final String CACHE_POLICY = "cachePolicy";
+    public static final String MAX_LIFESPAN = "maxLifespan";
+    public static final String EVICTION_HOUR = "evictionHour";
+    public static final String EVICTION_MINUTE = "evictionMinute";
+    public static final String EVICTION_DAY = "evictionDay";
+    public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
+    public static final String ENABLED = "enabled";
+
+    private transient CachePolicy cachePolicy;
+    private transient long maxLifespan = -1;
+    private transient int evictionHour = -1;
+    private transient int evictionMinute = -1;
+    private transient int evictionDay = -1;
+    private transient long cacheInvalidBefore = -1;
+    private transient Boolean enabled;
+
+    public CacheableStorageProviderModel() {
+    }
+
+    public CacheableStorageProviderModel(ComponentModel copy) {
+        super(copy);
+    }
+
+    public CachePolicy getCachePolicy() {
+        if (cachePolicy == null) {
+            String str = getConfig().getFirst(CACHE_POLICY);
+            if (str == null) return null;
+            cachePolicy = CachePolicy.valueOf(str);
+        }
+        return cachePolicy;
+    }
+
+    public void setCachePolicy(CachePolicy cachePolicy) {
+        this.cachePolicy = cachePolicy;
+        if (cachePolicy == null) {
+            getConfig().remove(CACHE_POLICY);
+
+        } else {
+            getConfig().putSingle(CACHE_POLICY, cachePolicy.name());
+        }
+    }
+
+    public long getMaxLifespan() {
+        if (maxLifespan < 0) {
+            String str = getConfig().getFirst(MAX_LIFESPAN);
+            if (str == null) return -1;
+            maxLifespan = Long.valueOf(str);
+        }
+        return maxLifespan;
+    }
+
+    public void setMaxLifespan(long maxLifespan) {
+        this.maxLifespan = maxLifespan;
+        getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan));
+    }
+
+    public int getEvictionHour() {
+        if (evictionHour < 0) {
+            String str = getConfig().getFirst(EVICTION_HOUR);
+            if (str == null) return -1;
+            evictionHour = Integer.valueOf(str);
+        }
+        return evictionHour;
+    }
+
+    public void setEvictionHour(int evictionHour) {
+        if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23");
+        this.evictionHour = evictionHour;
+        getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour));
+    }
+
+    public int getEvictionMinute() {
+        if (evictionMinute < 0) {
+            String str = getConfig().getFirst(EVICTION_MINUTE);
+            if (str == null) return -1;
+            evictionMinute = Integer.valueOf(str);
+        }
+        return evictionMinute;
+    }
+
+    public void setEvictionMinute(int evictionMinute) {
+        if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59");
+        this.evictionMinute = evictionMinute;
+        getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute));
+    }
+
+    public int getEvictionDay() {
+        if (evictionDay < 0) {
+            String str = getConfig().getFirst(EVICTION_DAY);
+            if (str == null) return -1;
+            evictionDay = Integer.valueOf(str);
+        }
+        return evictionDay;
+    }
+
+    public void setEvictionDay(int evictionDay) {
+        if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7");
+        this.evictionDay = evictionDay;
+        getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay));
+    }
+
+    public long getCacheInvalidBefore() {
+        if (cacheInvalidBefore < 0) {
+            String str = getConfig().getFirst(CACHE_INVALID_BEFORE);
+            if (str == null) return -1;
+            cacheInvalidBefore = Long.valueOf(str);
+        }
+        return cacheInvalidBefore;
+    }
+
+    public void setCacheInvalidBefore(long cacheInvalidBefore) {
+        this.cacheInvalidBefore = cacheInvalidBefore;
+        getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore));
+    }
+
+    public void setEnabled(boolean flag) {
+        enabled = flag;
+        getConfig().putSingle(ENABLED, Boolean.toString(flag));
+    }
+
+    public boolean isEnabled() {
+        if (enabled == null) {
+            String val = getConfig().getFirst(ENABLED);
+            if (val == null) {
+                enabled = true;
+            } else {
+                enabled = Boolean.valueOf(val);
+            }
+        }
+        return enabled;
+
+    }
+
+    public long getLifespan() {
+        UserStorageProviderModel.CachePolicy policy = getCachePolicy();
+        long lifespan = -1;
+        if (policy == null || policy == UserStorageProviderModel.CachePolicy.DEFAULT) {
+            lifespan = -1;
+        } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) {
+            if (getEvictionHour() > -1 && getEvictionMinute() > -1) {
+                lifespan = dailyTimeout(getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis();
+            }
+        } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
+            if (getEvictionDay() > 0 && getEvictionHour() > -1 && getEvictionMinute() > -1) {
+                lifespan = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute()) - Time.currentTimeMillis();
+            }
+        } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
+            lifespan = getMaxLifespan();
+        }
+        return lifespan;
+    }
+
+    public boolean shouldInvalidate(CachedObject cached) {
+        boolean invalidate = false;
+        if (!isEnabled()) {
+            invalidate = true;
+        } else {
+            CacheableStorageProviderModel.CachePolicy policy = getCachePolicy();
+            if (policy != null) {
+                //String currentTime = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(Time.currentTimeMillis()));
+                if (policy == CacheableStorageProviderModel.CachePolicy.NO_CACHE) {
+                    invalidate = true;
+                } else if (cached.getCacheTimestamp() < getCacheInvalidBefore()) {
+                    invalidate = true;
+                } else if (policy == CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN) {
+                    if (cached.getCacheTimestamp() + getMaxLifespan() < Time.currentTimeMillis()) {
+                        invalidate = true;
+                    }
+                } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_DAILY) {
+                    long dailyTimeout = dailyTimeout(getEvictionHour(), getEvictionMinute());
+                    dailyTimeout = dailyTimeout - (24 * 60 * 60 * 1000);
+                    //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(dailyTimeout));
+                    //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
+                    if (cached.getCacheTimestamp() <= dailyTimeout) {
+                        invalidate = true;
+                    }
+                } else if (policy == CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY) {
+                    int oneWeek = 7 * 24 * 60 * 60 * 1000;
+                    long weeklyTimeout = weeklyTimeout(getEvictionDay(), getEvictionHour(), getEvictionMinute());
+                    long lastTimeout = weeklyTimeout - oneWeek;
+                    //String timeout = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(weeklyTimeout));
+                    //String stamp = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date(cached.getCacheTimestamp()));
+                    if (cached.getCacheTimestamp() <= lastTimeout) {
+                        invalidate = true;
+                    }
+                }
+            }
+        }
+        return invalidate;
+    }
+
+
+    public static long dailyTimeout(int hour, int minute) {
+        Calendar cal = Calendar.getInstance();
+        Calendar cal2 = Calendar.getInstance();
+        cal.setTimeInMillis(Time.currentTimeMillis());
+        cal2.setTimeInMillis(Time.currentTimeMillis());
+        cal2.set(Calendar.HOUR_OF_DAY, hour);
+        cal2.set(Calendar.MINUTE, minute);
+        if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
+            int add = (24 * 60 * 60 * 1000);
+            cal.add(Calendar.MILLISECOND, add);
+        } else {
+            cal.add(Calendar.MILLISECOND, (int)(cal2.getTimeInMillis() - cal.getTimeInMillis()));
+        }
+        return cal.getTimeInMillis();
+    }
+
+    public static long weeklyTimeout(int day, int hour, int minute) {
+        Calendar cal = Calendar.getInstance();
+        Calendar cal2 = Calendar.getInstance();
+        cal.setTimeInMillis(Time.currentTimeMillis());
+        cal2.setTimeInMillis(Time.currentTimeMillis());
+        cal2.set(Calendar.HOUR_OF_DAY, hour);
+        cal2.set(Calendar.MINUTE, minute);
+        cal2.set(Calendar.DAY_OF_WEEK, day);
+        if (cal2.getTimeInMillis() < cal.getTimeInMillis()) {
+            int add = (7 * 24 * 60 * 60 * 1000);
+            cal2.add(Calendar.MILLISECOND, add);
+        }
+
+        return cal2.getTimeInMillis();
+    }
+
+
+
+    public enum CachePolicy {
+        NO_CACHE,
+        DEFAULT,
+        EVICT_DAILY,
+        EVICT_WEEKLY,
+        MAX_LIFESPAN
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java
new file mode 100644
index 0000000..7f04e5f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientLookupProvider.java
@@ -0,0 +1,31 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+
+/**
+ * Abstraction interface for lookoup of clients by id and clientId.  These methods required for participating in login flows.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientLookupProvider {
+    ClientModel getClientById(String id, RealmModel realm);
+    ClientModel getClientByClientId(String clientId, RealmModel realm);
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
new file mode 100644
index 0000000..c0773a6
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProvider.java
@@ -0,0 +1,72 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.provider.Provider;
+import org.keycloak.storage.client.ClientLookupProvider;
+
+/**
+ * Base interface for components that want to provide an alternative storage mechanism for clients
+ *
+ * This is currently a private incomplete SPI.  Please discuss on dev list if you want us to complete it or want to do the work yourself.
+ * This work is described in KEYCLOAK-6408 JIRA issue.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ClientStorageProvider extends Provider, ClientLookupProvider {
+
+
+    /**
+     * Callback when a realm is removed.  Implement this if, for example, you want to do some
+     * cleanup in your user storage when a realm is removed
+     *
+     * @param realm
+     */
+    default
+    void preRemove(RealmModel realm) {
+
+    }
+
+    /**
+     * Callback when a group is removed.  Allows you to do things like remove a user
+     * group mapping in your external store if appropriate
+     *
+     * @param realm
+     * @param group
+     */
+    default
+    void preRemove(RealmModel realm, GroupModel group) {
+
+    }
+
+    /**
+     * Callback when a role is removed.  Allows you to do things like remove a user
+     * role mapping in your external store if appropriate
+
+     * @param realm
+     * @param role
+     */
+    default
+    void preRemove(RealmModel realm, RoleModel role) {
+
+    }
+}
+
diff --git a/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java
new file mode 100755
index 0000000..54093af
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/storage/client/ClientStorageProviderModel.java
@@ -0,0 +1,60 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.CacheableStorageProviderModel;
+
+/**
+ * Stored configuration of a Client Storage provider instance.
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class ClientStorageProviderModel extends CacheableStorageProviderModel {
+
+    public static final String ENABLED = "enabled";
+
+    public ClientStorageProviderModel() {
+        setProviderType(ClientStorageProvider.class.getName());
+    }
+
+    public ClientStorageProviderModel(ComponentModel copy) {
+        super(copy);
+    }
+
+    private transient Boolean enabled;
+
+     public void setEnabled(boolean flag) {
+        enabled = flag;
+        getConfig().putSingle(ENABLED, Boolean.toString(flag));
+    }
+
+
+    public boolean isEnabled() {
+        if (enabled == null) {
+            String val = getConfig().getFirst(ENABLED);
+            if (val == null) {
+                enabled = true;
+            } else {
+                enabled = Boolean.valueOf(val);
+            }
+        }
+        return enabled;
+
+    }
+}
diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
index fbbc406..3a2a141 100644
--- a/server-spi/src/main/java/org/keycloak/storage/StorageId.java
+++ b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
@@ -17,6 +17,7 @@
 package org.keycloak.storage;
 
 import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.UserModel;
 
 import java.io.Serializable;
@@ -75,8 +76,15 @@ public class StorageId implements Serializable {
     public static boolean isLocalStorage(UserModel user) {
         return new StorageId(user.getId()).getProviderId() == null;
     }
-    public static boolean isLocalStorage(String userId) {
-        return new StorageId(userId).getProviderId() == null;
+    public static boolean isLocalStorage(String id) {
+        return new StorageId(id).getProviderId() == null;
+    }
+
+    public static String resolveProviderId(ClientModel client) {
+        return new StorageId(client.getId()).getProviderId();
+    }
+    public static boolean isLocalStorage(ClientModel client) {
+        return new StorageId(client.getId()).getProviderId() == null;
     }
     public boolean isLocal() {
         return getProviderId() == null;
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
index 1ec06a6..e145c40 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
@@ -18,7 +18,6 @@
 package org.keycloak.storage;
 
 import org.keycloak.component.ComponentModel;
-import org.keycloak.component.PrioritizedComponentModel;
 
 /**
  * Stored configuration of a User Storage provider instance.
@@ -26,27 +25,12 @@ import org.keycloak.component.PrioritizedComponentModel;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
  */
-public class UserStorageProviderModel extends PrioritizedComponentModel {
-
-    public static final String CACHE_POLICY = "cachePolicy";
-    public static final String MAX_LIFESPAN = "maxLifespan";
-    public static final String EVICTION_HOUR = "evictionHour";
-    public static final String EVICTION_MINUTE = "evictionMinute";
-    public static final String EVICTION_DAY = "evictionDay";
-    public static final String CACHE_INVALID_BEFORE = "cacheInvalidBefore";
+public class UserStorageProviderModel extends CacheableStorageProviderModel {
+
     public static final String IMPORT_ENABLED = "importEnabled";
     public static final String FULL_SYNC_PERIOD = "fullSyncPeriod";
     public static final String CHANGED_SYNC_PERIOD = "changedSyncPeriod";
     public static final String LAST_SYNC = "lastSync";
-    public static final String ENABLED = "enabled";
-
-    public static enum CachePolicy {
-        NO_CACHE,
-        DEFAULT,
-        EVICT_DAILY,
-        EVICT_WEEKLY,
-        MAX_LIFESPAN
-    }
 
     public UserStorageProviderModel() {
         setProviderType(UserStorageProvider.class.getName());
@@ -60,105 +44,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     private transient Integer changedSyncPeriod;
     private transient Integer lastSync;
     private transient Boolean importEnabled;
-    private transient Boolean enabled;
-    private transient CachePolicy cachePolicy;
-    private transient long maxLifespan = -1;
-    private transient int evictionHour = -1;
-    private transient int evictionMinute = -1;
-    private transient int evictionDay = -1;
-    private transient long cacheInvalidBefore = -1;
-
-    public CachePolicy getCachePolicy() {
-        if (cachePolicy == null) {
-            String str = getConfig().getFirst(CACHE_POLICY);
-            if (str == null) return null;
-            cachePolicy = CachePolicy.valueOf(str);
-        }
-        return cachePolicy;
-    }
-
-    public void setCachePolicy(CachePolicy cachePolicy) {
-        this.cachePolicy = cachePolicy;
-        if (cachePolicy == null) {
-            getConfig().remove(CACHE_POLICY);
-
-        } else {
-            getConfig().putSingle(CACHE_POLICY, cachePolicy.name());
-        }
-    }
-
-    public long getMaxLifespan() {
-        if (maxLifespan < 0) {
-            String str = getConfig().getFirst(MAX_LIFESPAN);
-            if (str == null) return -1;
-            maxLifespan = Long.valueOf(str);
-        }
-        return maxLifespan;
-    }
-
-    public void setMaxLifespan(long maxLifespan) {
-        this.maxLifespan = maxLifespan;
-        getConfig().putSingle(MAX_LIFESPAN, Long.toString(maxLifespan));
-    }
-
-    public int getEvictionHour() {
-        if (evictionHour < 0) {
-            String str = getConfig().getFirst(EVICTION_HOUR);
-            if (str == null) return -1;
-            evictionHour = Integer.valueOf(str);
-        }
-        return evictionHour;
-    }
-
-    public void setEvictionHour(int evictionHour) {
-        if (evictionHour > 23 || evictionHour < 0) throw new IllegalArgumentException("Must be between 0 and 23");
-        this.evictionHour = evictionHour;
-        getConfig().putSingle(EVICTION_HOUR, Integer.toString(evictionHour));
-    }
-
-    public int getEvictionMinute() {
-        if (evictionMinute < 0) {
-            String str = getConfig().getFirst(EVICTION_MINUTE);
-            if (str == null) return -1;
-            evictionMinute = Integer.valueOf(str);
-        }
-        return evictionMinute;
-    }
-
-    public void setEvictionMinute(int evictionMinute) {
-        if (evictionMinute > 59 || evictionMinute < 0) throw new IllegalArgumentException("Must be between 0 and 59");
-        this.evictionMinute = evictionMinute;
-        getConfig().putSingle(EVICTION_MINUTE, Integer.toString(evictionMinute));
-    }
-
-    public int getEvictionDay() {
-        if (evictionDay < 0) {
-            String str = getConfig().getFirst(EVICTION_DAY);
-            if (str == null) return -1;
-            evictionDay = Integer.valueOf(str);
-        }
-        return evictionDay;
-    }
-
-    public void setEvictionDay(int evictionDay) {
-        if (evictionDay > 7 || evictionDay < 1) throw new IllegalArgumentException("Must be between 1 and 7");
-        this.evictionDay = evictionDay;
-        getConfig().putSingle(EVICTION_DAY, Integer.toString(evictionDay));
-    }
-
-    public long getCacheInvalidBefore() {
-        if (cacheInvalidBefore < 0) {
-            String str = getConfig().getFirst(CACHE_INVALID_BEFORE);
-            if (str == null) return -1;
-            cacheInvalidBefore = Long.valueOf(str);
-        }
-        return cacheInvalidBefore;
-    }
-
-    public void setCacheInvalidBefore(long cacheInvalidBefore) {
-        this.cacheInvalidBefore = cacheInvalidBefore;
-        getConfig().putSingle(CACHE_INVALID_BEFORE, Long.toString(cacheInvalidBefore));
-    }
 
     public boolean isImportEnabled() {
         if (importEnabled == null) {
@@ -178,24 +63,6 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
         getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(flag));
     }
 
-    public void setEnabled(boolean flag) {
-        enabled = flag;
-        getConfig().putSingle(ENABLED, Boolean.toString(flag));
-    }
-
-
-    public boolean isEnabled() {
-        if (enabled == null) {
-            String val = getConfig().getFirst(ENABLED);
-            if (val == null) {
-                enabled = true;
-            } else {
-                enabled = Boolean.valueOf(val);
-            }
-        }
-        return enabled;
-
-    }
 
     public int getFullSyncPeriod() {
         if (fullSyncPeriod == null) {
diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
index 61ae1be..ce71dee 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java
@@ -25,7 +25,7 @@ import org.keycloak.models.RealmProvider;
  */
 public interface CacheRealmProvider extends RealmProvider {
     void clear();
-    RealmProvider getDelegate();
+    RealmProvider getRealmDelegate();
 
     void registerRealmInvalidation(String id, String name);
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 0dad16a..2e7d3ef 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -485,6 +485,8 @@ public class ModelToRepresentation {
     public static ClientRepresentation toRepresentation(ClientModel clientModel) {
         ClientRepresentation rep = new ClientRepresentation();
         rep.setId(clientModel.getId());
+        String providerId = StorageId.resolveProviderId(clientModel);
+        rep.setOrigin(providerId);
         rep.setClientId(clientModel.getClientId());
         rep.setName(clientModel.getName());
         rep.setDescription(clientModel.getDescription());
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java
new file mode 100644
index 0000000..db2a9ad
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractClientStorageAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.storage.StorageId;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Helper base class for ClientModel implementations for ClientStorageProvider implementations.
+ *
+ * Contains default implementations of some methods
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractClientStorageAdapter extends UnsupportedOperationsClientStorageAdapter {
+    protected KeycloakSession session;
+    protected RealmModel realm;
+    protected ClientStorageProviderModel component;
+    private StorageId storageId;
+
+
+    public AbstractClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) {
+        this.session = session;
+        this.realm = realm;
+        this.component = component;
+    }
+
+    /**
+     * Creates federated id based on getClientId() method
+     *
+     * @return
+     */
+    @Override
+    public String getId() {
+        if (storageId == null) {
+            storageId = new StorageId(component.getId(), getClientId());
+        }
+        return storageId.getId();
+    }
+
+    @Override
+    public final RealmModel getRealm() {
+        return realm;
+    }
+
+
+    /**
+     * This method really isn't used by anybody anywhere.  Legacy feature never supported.
+     *
+     * @return
+     */
+    @Override
+    public boolean isSurrogateAuthRequired() {
+        return false;
+    }
+
+    /**
+     * This method really isn't used by anybody anywhere.  Legacy feature never supported.
+     *
+     * @return
+     */
+    @Override
+    public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+        // do nothing, we don't do anything with this.
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public Map<String, Integer> getRegisteredNodes() {
+        return Collections.EMPTY_MAP;
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public void registerNode(String nodeHost, int registrationTime) {
+        // do nothing
+    }
+
+    /**
+     * This is for logout.  Empty implementation for now.  Can override if you can store this information somewhere.
+     *
+     * @return
+     */
+    @Override
+    public void unregisterNode(String nodeHost) {
+        // do nothing
+    }
+
+    /**
+     * Overriding implementations should call super.updateClient() as this fires off an update event.
+     *
+     */
+    @Override
+    public void updateClient() {
+        session.getKeycloakSessionFactory().publish(new RealmModel.ClientUpdatedEvent() {
+
+            @Override
+            public ClientModel getUpdatedClient() {
+                return AbstractClientStorageAdapter.this;
+            }
+
+            @Override
+            public KeycloakSession getKeycloakSession() {
+                return session;
+            }
+        });
+
+    }
+
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java
new file mode 100644
index 0000000..d8e6bd4
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/AbstractReadOnlyClientStorageAdapter.java
@@ -0,0 +1,280 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.storage.ReadOnlyException;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractReadOnlyClientStorageAdapter extends AbstractClientStorageAdapter {
+    public AbstractReadOnlyClientStorageAdapter(KeycloakSession session, RealmModel realm, ClientStorageProviderModel component) {
+        super(session, realm, component);
+    }
+
+    @Override
+    public void setClientId(String clientId) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setName(String name) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setDescription(String description) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setWebOrigins(Set<String> webOrigins) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addWebOrigin(String webOrigin) {
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void removeWebOrigin(String webOrigin) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRedirectUris(Set<String> redirectUris) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addRedirectUri(String redirectUri) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeRedirectUri(String redirectUri) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setManagementUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRootUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setBaseUrl(String url) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setBearerOnly(boolean only) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setNodeReRegistrationTimeout(int timeout) {
+
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void setClientAuthenticatorType(String clientAuthenticatorType) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setSecret(String secret) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setRegistrationToken(String registrationToken) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setProtocol(String protocol) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setAttribute(String name, String value) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeAttribute(String name) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void removeAuthenticationFlowBindingOverride(String binding) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setFrontchannelLogout(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setPublicClient(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setConsentRequired(boolean consentRequired) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setClientTemplate(ClientTemplateModel template) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setUseTemplateScope(boolean flag) {
+
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void setUseTemplateMappers(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setUseTemplateConfig(boolean flag) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setNotBefore(int notBefore) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
+        throw new ReadOnlyException("client is read only for this update");
+    }
+
+    @Override
+    public void removeProtocolMapper(ProtocolMapperModel mapping) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void updateProtocolMapper(ProtocolMapperModel mapping) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void setFullScopeAllowed(boolean value) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void addScopeMapping(RoleModel role) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+
+    @Override
+    public void deleteScopeMapping(RoleModel role) {
+        throw new ReadOnlyException("client is read only for this update");
+
+    }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java
new file mode 100755
index 0000000..e9f8ee7
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderFactory.java
@@ -0,0 +1,118 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.Config;
+import org.keycloak.component.ComponentFactory;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+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 interface ClientStorageProviderFactory<T extends ClientStorageProvider> extends ComponentFactory<T, ClientStorageProvider> {
+
+
+    /**
+     * called per Keycloak transaction.
+     *
+     * @param session
+     * @param model
+     * @return
+     */
+    T create(KeycloakSession session, ComponentModel model);
+
+    /**
+     * This is the name of the provider and will be showed in the admin console as an option.
+     *
+     * @return
+     */
+    @Override
+    String getId();
+
+    @Override
+    default void init(Config.Scope config) {
+
+    }
+
+    @Override
+    default void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    default void close() {
+
+    }
+
+    @Override
+    default String getHelpText() {
+        return "";
+    }
+
+    @Override
+    default List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+
+    }
+
+    /**
+     * Called when ClientStorageProviderModel is created.  This allows you to do initialization of any additional configuration
+     * you need to add.
+     *
+     * @param session
+     * @param realm
+     * @param model
+     */
+    @Override
+    default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+
+    /**
+     * configuration properties that are common across all UserStorageProvider implementations
+     *
+     * @return
+     */
+    @Override
+    default
+    List<ProviderConfigProperty> getCommonProviderConfigProperties() {
+        return ClientStorageProviderSpi.commonConfig();
+    }
+
+    @Override
+    default
+    Map<String, Object> getTypeMetadata() {
+        Map<String, Object> metadata = new HashMap<>();
+        return metadata;
+    }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java
new file mode 100755
index 0000000..bf1146d
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/ClientStorageProviderSpi.java
@@ -0,0 +1,83 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientStorageProviderSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return true;
+    }
+
+    @Override
+    public String getName() {
+        return "client-storage";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return ClientStorageProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return ClientStorageProviderFactory.class;
+    }
+
+    private static final List<ProviderConfigProperty> commonConfig;
+
+    static {
+        List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
+                .property()
+                .name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
+                .property()
+                .name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
+                 .property()
+                .name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
+                .name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add()
+                .build();
+        commonConfig = Collections.unmodifiableList(config);
+    }
+
+    public static List<ProviderConfigProperty> commonConfig() {
+        return commonConfig;
+
+    }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java
new file mode 100644
index 0000000..4de10a6
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/storage/client/UnsupportedOperationsClientStorageAdapter.java
@@ -0,0 +1,82 @@
+/*
+ * 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.storage.client;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RoleModel;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base helper class.  Unsupported operations are implemented here that throw exception on invocation.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class UnsupportedOperationsClientStorageAdapter implements ClientModel {
+    @Override
+    public final RoleModel getRole(String name) {
+        return null;
+    }
+
+    @Override
+    public final RoleModel addRole(String name) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final RoleModel addRole(String id, String name) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final boolean removeRole(RoleModel role) {
+        throw new ModelException("Unsupported operation");
+    }
+
+    @Override
+    public final Set<RoleModel> getRoles() {
+        return Collections.EMPTY_SET;
+    }
+
+    @Override
+    public final List<String> getDefaultRoles() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public final void addDefaultRole(String name) {
+        throw new ModelException("Unsupported operation");
+
+    }
+
+    @Override
+    public final void updateDefaultRoles(String... defaultRoles) {
+        throw new ModelException("Unsupported operation");
+
+    }
+
+    @Override
+    public final void removeDefaultRoles(String... defaultRoles) {
+        throw new ModelException("Unsupported operation");
+    }
+
+
+}
diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index b781ca3..a63b206 100755
--- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -70,4 +70,5 @@ org.keycloak.transaction.TransactionManagerLookupSpi
 org.keycloak.credential.hash.PasswordHashSpi
 org.keycloak.credential.CredentialSpi
 org.keycloak.keys.PublicKeyStorageSpi
-org.keycloak.keys.KeySpi
\ No newline at end of file
+org.keycloak.keys.KeySpi
+org.keycloak.storage.client.ClientStorageProviderSpi
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 0cc81c1..6cabf76 100644
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
@@ -20,6 +20,7 @@ import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.credential.UserCredentialStoreManager;
 import org.keycloak.keys.DefaultKeyManager;
+import org.keycloak.models.ClientProvider;
 import org.keycloak.models.KeycloakContext;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -35,6 +36,7 @@ import org.keycloak.models.cache.UserCache;
 import org.keycloak.provider.Provider;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.storage.ClientStorageManager;
 import org.keycloak.storage.UserStorageManager;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.theme.DefaultThemeManager;
@@ -58,6 +60,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
     private final Map<String, Object> attributes = new HashMap<>();
     private RealmProvider model;
     private UserStorageManager userStorageManager;
+    private ClientStorageManager clientStorageManager;
     private UserCredentialStoreManager userCredentialStorageManager;
     private UserSessionProvider sessionProvider;
     private AuthenticationSessionProvider authenticationSessionProvider;
@@ -136,6 +139,23 @@ public class DefaultKeycloakSession implements KeycloakSession {
     }
 
     @Override
+    public RealmProvider realmLocalStorage() {
+        return getProvider(RealmProvider.class);
+    }
+
+    @Override
+    public ClientProvider clientLocalStorage() {
+        return realmLocalStorage();
+    }
+
+    @Override
+    public ClientProvider clientStorageManager() {
+        if (clientStorageManager == null) clientStorageManager = new ClientStorageManager(this);
+        return clientStorageManager;
+    }
+
+
+    @Override
     public UserProvider userStorageManager() {
         if (userStorageManager == null) userStorageManager = new UserStorageManager(this);
         return userStorageManager;
@@ -232,6 +252,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
         return model;
     }
 
+
     @Override
     public UserSessionProvider sessions() {
         if (sessionProvider == null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index c0ea7df..580a0bc 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -98,7 +98,7 @@ public class ClientsResource {
     public List<ClientRepresentation> getClients(@QueryParam("clientId") String clientId, @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly) {
         List<ClientRepresentation> rep = new ArrayList<>();
 
-        if (clientId == null) {
+        if (clientId == null || clientId.trim().equals("")) {
             List<ClientModel> clientModels = realm.getClients();
             auth.clients().requireList();
             boolean view = auth.clients().canView();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java
new file mode 100644
index 0000000..6c8561c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientStorageProviderResource.java
@@ -0,0 +1,111 @@
+/*
+ * 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.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @resource User Storage Provider
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientStorageProviderResource {
+    private static final Logger logger = Logger.getLogger(ClientStorageProviderResource.class);
+
+    protected RealmModel realm;
+
+    protected AdminPermissionEvaluator auth;
+
+    protected AdminEventBuilder adminEvent;
+
+    @Context
+    protected ClientConnection clientConnection;
+
+    @Context
+    protected UriInfo uriInfo;
+
+    @Context
+    protected KeycloakSession session;
+
+    @Context
+    protected HttpHeaders headers;
+
+    public ClientStorageProviderResource(RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
+        this.auth = auth;
+        this.realm = realm;
+        this.adminEvent = adminEvent;
+    }
+
+    /**
+     * Need this for admin console to display simple name of provider when displaying client detail
+     *
+     * KEYCLOAK-4328
+     *
+     * @param id
+     * @return
+     */
+    @GET
+    @Path("{id}/name")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, String> getSimpleName(@PathParam("id") String id) {
+        auth.clients().requireList();
+
+        ComponentModel model = realm.getComponent(id);
+        if (model == null) {
+            throw new NotFoundException("Could not find component");
+        }
+        if (!model.getProviderType().equals(ClientStorageProvider.class.getName())) {
+            throw new NotFoundException("found, but not a ClientStorageProvider");
+        }
+
+        Map<String, String> data = new HashMap<>();
+        data.put("id", model.getId());
+        data.put("name", model.getName());
+        return data;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index 1c5978e..00726e4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
@@ -31,6 +31,7 @@ import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.services.ForbiddenException;
+import org.keycloak.storage.StorageId;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -634,8 +635,8 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
     public Map<String, Boolean> getAccess(ClientModel client) {
         Map<String, Boolean> map = new HashMap<>();
         map.put("view", canView(client));
-        map.put("manage", canManage(client));
-        map.put("configure", canConfigure(client));
+        map.put("manage", StorageId.isLocalStorage(client) && canManage(client));
+        map.put("configure", StorageId.isLocalStorage(client) && canConfigure(client));
         return map;
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 88af6f6..3a82baa 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -100,6 +100,7 @@ import java.security.cert.X509Certificate;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -504,17 +505,38 @@ public class RealmAdminResource {
     public List<Map<String, String>> getClientSessionStats() {
         auth.realm().requireViewRealm();
 
-        List<Map<String, String>> data = new LinkedList<Map<String, String>>();
-        for (ClientModel client : realm.getClients()) {
-            long size = session.sessions().getActiveUserSessions(client.getRealm(), client);
-            if (size == 0) continue;
-            Map<String, String> map = new HashMap<>();
-            map.put("id", client.getId());
-            map.put("clientId", client.getClientId());
-            map.put("active", size + "");
-            data.add(map);
-        }
-        return data;
+        Map<String, Map<String, String>> data = new HashMap();
+        {
+            Map<String, Long> activeCount =session.sessions().getActiveClientSessionStats(realm, false);
+            for (Map.Entry<String, Long> entry : activeCount.entrySet()) {
+                Map<String, String> map = new HashMap<>();
+                ClientModel client = realm.getClientById(entry.getKey());
+                map.put("id", client.getId());
+                map.put("clientId", client.getClientId());
+                map.put("active", entry.getValue().toString());
+                map.put("offline", "0");
+                data.put(client.getId(), map);
+
+            }
+        }
+        {
+            Map<String, Long> offlineCount = session.sessions().getActiveClientSessionStats(realm, true);
+            for (Map.Entry<String, Long> entry : offlineCount.entrySet()) {
+                Map<String, String> map = data.get(entry.getKey());
+                if (map == null) {
+                    map = new HashMap<>();
+                    ClientModel client = realm.getClientById(entry.getKey());
+                    map.put("id", client.getId());
+                    map.put("clientId", client.getClientId());
+                    map.put("active", "0");
+                    data.put(client.getId(), map);
+                }
+                map.put("offline", entry.getValue().toString());
+            }
+        }
+        List<Map<String, String>> result = new LinkedList<>();
+        for (Map<String, String> item : data.values()) result.add(item);
+        return result;
     }
 
     /**
diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
new file mode 100644
index 0000000..d60d2f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java
@@ -0,0 +1,222 @@
+/*
+ * 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.storage;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.reflections.Types;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.storage.client.ClientLookupProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.client.ClientStorageProviderFactory;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ClientStorageManager implements ClientProvider {
+    private static final Logger logger = Logger.getLogger(ClientStorageManager.class);
+
+    protected KeycloakSession session;
+
+    public static boolean isStorageProviderEnabled(RealmModel realm, String providerId) {
+        ClientStorageProviderModel model = getStorageProviderModel(realm, providerId);
+        return model.isEnabled();
+    }
+
+    public static ClientStorageProviderModel getStorageProviderModel(RealmModel realm, String componentId) {
+        ComponentModel model = realm.getComponent(componentId);
+        if (model == null) return null;
+        return new ClientStorageProviderModel(model);
+    }
+
+    public static ClientStorageProvider getStorageProvider(KeycloakSession session, RealmModel realm, String componentId) {
+        ComponentModel model = realm.getComponent(componentId);
+        if (model == null) return null;
+        ClientStorageProviderModel storageModel = new ClientStorageProviderModel(model);
+        ClientStorageProviderFactory factory = (ClientStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId());
+        if (factory == null) {
+            throw new ModelException("Could not find ClientStorageProviderFactory for: " + model.getProviderId());
+        }
+        return getStorageProviderInstance(session, storageModel, factory);
+    }
+
+
+    public static List<ClientStorageProviderModel> getStorageProviders(RealmModel realm) {
+        return realm.getClientStorageProviders();
+    }
+
+    public static ClientStorageProvider getStorageProviderInstance(KeycloakSession session, ClientStorageProviderModel model, ClientStorageProviderFactory factory) {
+        ClientStorageProvider instance = (ClientStorageProvider)session.getAttribute(model.getId());
+        if (instance != null) return instance;
+        instance = factory.create(session, model);
+        if (instance == null) {
+            throw new IllegalStateException("ClientStorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance");
+        }
+        session.enlistForClose(instance);
+        session.setAttribute(model.getId(), instance);
+        return instance;
+    }
+
+
+    public static <T> List<T> getStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
+        List<T> list = new LinkedList<>();
+        for (ClientStorageProviderModel model : getStorageProviders(realm)) {
+            ClientStorageProviderFactory factory = (ClientStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId());
+            if (factory == null) {
+                logger.warnv("Configured ClientStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName());
+                continue;
+            }
+            if (Types.supports(type, factory, ClientStorageProviderFactory.class)) {
+                list.add(type.cast(getStorageProviderInstance(session, model, factory)));
+            }
+
+
+        }
+        return list;
+    }
+
+
+    public static <T> List<T> getEnabledStorageProviders(KeycloakSession session, RealmModel realm, Class<T> type) {
+        List<T> list = new LinkedList<>();
+        for (ClientStorageProviderModel model : getStorageProviders(realm)) {
+            if (!model.isEnabled()) continue;
+            ClientStorageProviderFactory factory = (ClientStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(ClientStorageProvider.class, model.getProviderId());
+            if (factory == null) {
+                logger.warnv("Configured ClientStorageProvider {0} of provider id {1} does not exist in realm {2}", model.getName(), model.getProviderId(), realm.getName());
+                continue;
+            }
+            if (Types.supports(type, factory, ClientStorageProviderFactory.class)) {
+                list.add(type.cast(getStorageProviderInstance(session, model, factory)));
+            }
+
+
+        }
+        return list;
+    }
+
+
+    public ClientStorageManager(KeycloakSession session) {
+        this.session = session;
+    }
+
+    @Override
+    public ClientModel getClientById(String id, RealmModel realm) {
+        StorageId storageId = new StorageId(id);
+        if (storageId.getProviderId() == null) {
+            return session.clientLocalStorage().getClientById(id, realm);
+        }
+        ClientLookupProvider provider = (ClientLookupProvider)getStorageProvider(session, realm, storageId.getProviderId());
+        if (provider == null) return null;
+        if (!isStorageProviderEnabled(realm, storageId.getProviderId())) return null;
+        return provider.getClientById(id, realm);
+    }
+
+    @Override
+    public ClientModel getClientByClientId(String clientId, RealmModel realm) {
+        ClientModel client = session.clientLocalStorage().getClientByClientId(clientId, realm);
+        if (client != null) {
+            return client;
+        }
+        for (ClientLookupProvider provider : getEnabledStorageProviders(session, realm, ClientLookupProvider.class)) {
+            client = provider.getClientByClientId(clientId, realm);
+            if (client != null) return client;
+        }
+        return null;
+    }
+
+
+    @Override
+    public ClientModel addClient(RealmModel realm, String clientId) {
+        return session.clientLocalStorage().addClient(realm, clientId);
+    }
+
+    @Override
+    public ClientModel addClient(RealmModel realm, String id, String clientId) {
+        return session.clientLocalStorage().addClient(realm, id, clientId);
+    }
+
+
+
+
+    @Override
+    public List<ClientModel> getClients(RealmModel realm) {
+       return session.clientLocalStorage().getClients(realm);
+    }
+
+    @Override
+    public RoleModel addClientRole(RealmModel realm, ClientModel client, String name) {
+        if (!StorageId.isLocalStorage(client.getId())) {
+            throw new RuntimeException("Federated clients do not support this operation");
+        }
+        return session.clientLocalStorage().addClientRole(realm, client, name);
+    }
+
+    @Override
+    public RoleModel addClientRole(RealmModel realm, ClientModel client, String id, String name) {
+        if (!StorageId.isLocalStorage(client.getId())) {
+            throw new RuntimeException("Federated clients do not support this operation");
+        }
+        return session.clientLocalStorage().addClientRole(realm, client, id, name);
+    }
+
+    @Override
+    public RoleModel getClientRole(RealmModel realm, ClientModel client, String name) {
+        if (!StorageId.isLocalStorage(client.getId())) {
+            //throw new RuntimeException("Federated clients do not support this operation");
+            return null;
+        }
+        return session.clientLocalStorage().getClientRole(realm, client, name);
+    }
+
+    @Override
+    public Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client) {
+        if (!StorageId.isLocalStorage(client.getId())) {
+            //throw new RuntimeException("Federated clients do not support this operation");
+            return Collections.EMPTY_SET;
+        }
+        return session.clientLocalStorage().getClientRoles(realm, client);
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public boolean removeClient(String id, RealmModel realm) {
+        if (!StorageId.isLocalStorage(id)) {
+            throw new RuntimeException("Federated clients do not support this operation");
+        }
+        return session.clientLocalStorage().removeClient(id, realm);
+    }
+
+
+
+}
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 16d14e0..4801553 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -40,6 +40,7 @@ import org.keycloak.models.cache.UserCache;
 import org.keycloak.models.utils.ComponentUtil;
 import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
 import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.storage.client.ClientStorageProvider;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.storage.user.ImportedUserValidation;
 import org.keycloak.storage.user.UserBulkUpdateProvider;
@@ -696,6 +697,11 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
+        if (component.getProviderType().equals(ClientStorageProvider.class.getName())) {
+            localStorage().preRemove(realm, component);
+            if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, component);
+            return;
+        }
         if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
         localStorage().preRemove(realm, component);
         if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, component);
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
new file mode 100644
index 0000000..672cb1e
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProvider.java
@@ -0,0 +1,282 @@
+/*
+ * 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;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientTemplateModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.client.AbstractReadOnlyClientStorageAdapter;
+import org.keycloak.storage.client.ClientLookupProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class HardcodedClientStorageProvider implements ClientStorageProvider, ClientLookupProvider {
+    protected KeycloakSession session;
+    protected ClientStorageProviderModel component;
+    protected String clientId;
+    protected String redirectUri;
+    protected boolean consent;
+
+    public HardcodedClientStorageProvider(KeycloakSession session, ClientStorageProviderModel component) {
+        this.session = session;
+        this.component = component;
+        this.clientId = component.getConfig().getFirst(HardcodedClientStorageProviderFactory.CLIENT_ID);
+        this.redirectUri = component.getConfig().getFirst(HardcodedClientStorageProviderFactory.REDIRECT_URI);
+        this.consent = "true".equals(component.getConfig().getFirst(HardcodedClientStorageProviderFactory.CONSENT));
+    }
+
+    @Override
+    public ClientModel getClientById(String id, RealmModel realm) {
+        StorageId storageId = new StorageId(id);
+        final String clientId = storageId.getExternalId();
+        if (this.clientId.equals(clientId)) return new ClientAdapter(realm);
+        return null;
+    }
+
+    @Override
+    public ClientModel getClientByClientId(String clientId, RealmModel realm) {
+        if (this.clientId.equals(clientId)) return new ClientAdapter(realm);
+        return null;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    public class ClientAdapter extends AbstractReadOnlyClientStorageAdapter {
+
+        public ClientAdapter(RealmModel realm) {
+            super(HardcodedClientStorageProvider.this.session, realm, HardcodedClientStorageProvider.this.component);
+        }
+
+        @Override
+        public String getClientId() {
+            return clientId;
+        }
+
+        @Override
+        public String getName() {
+            return "Federated Client";
+        }
+
+        @Override
+        public String getDescription() {
+            return "Pulled in from client storage provider";
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return true;
+        }
+
+        @Override
+        public Set<String> getWebOrigins() {
+            return Collections.EMPTY_SET;
+        }
+
+        @Override
+        public Set<String> getRedirectUris() {
+            HashSet<String> set = new HashSet<>();
+            set.add(redirectUri);
+            return set;
+        }
+
+        @Override
+        public String getManagementUrl() {
+            return null;
+        }
+
+        @Override
+        public String getRootUrl() {
+            return null;
+        }
+
+        @Override
+        public String getBaseUrl() {
+            return null;
+        }
+
+        @Override
+        public boolean isBearerOnly() {
+            return false;
+        }
+
+        @Override
+        public int getNodeReRegistrationTimeout() {
+            return 0;
+        }
+
+        @Override
+        public String getClientAuthenticatorType() {
+            return null;
+        }
+
+        @Override
+        public boolean validateSecret(String secret) {
+            return "password".equals(secret);
+        }
+
+        @Override
+        public String getSecret() {
+            return "password";
+        }
+
+        @Override
+        public String getRegistrationToken() {
+            return null;
+        }
+
+        @Override
+        public String getProtocol() {
+            return "openid-connect";
+        }
+
+        @Override
+        public String getAttribute(String name) {
+            return null;
+        }
+
+        @Override
+        public Map<String, String> getAttributes() {
+            return Collections.EMPTY_MAP;
+        }
+
+        @Override
+        public String getAuthenticationFlowBindingOverride(String binding) {
+            return null;
+        }
+
+        @Override
+        public Map<String, String> getAuthenticationFlowBindingOverrides() {
+            return Collections.EMPTY_MAP;
+        }
+
+        @Override
+        public boolean isFrontchannelLogout() {
+            return false;
+        }
+
+        @Override
+        public boolean isPublicClient() {
+            return false;
+        }
+
+        @Override
+        public boolean isConsentRequired() {
+            return consent;
+        }
+
+        @Override
+        public boolean isStandardFlowEnabled() {
+            return true;
+        }
+
+        @Override
+        public boolean isImplicitFlowEnabled() {
+            return true;
+        }
+
+        @Override
+        public boolean isDirectAccessGrantsEnabled() {
+            return true;
+        }
+
+        @Override
+        public boolean isServiceAccountsEnabled() {
+            return false;
+        }
+
+        @Override
+        public ClientTemplateModel getClientTemplate() {
+            return null;
+        }
+
+        @Override
+        public boolean useTemplateScope() {
+            return false;
+        }
+
+        @Override
+        public boolean useTemplateMappers() {
+            return false;
+        }
+
+        @Override
+        public boolean useTemplateConfig() {
+            return false;
+        }
+
+        @Override
+        public int getNotBefore() {
+            return 0;
+        }
+
+        @Override
+        public Set<ProtocolMapperModel> getProtocolMappers() {
+            return Collections.EMPTY_SET;
+        }
+
+        @Override
+        public ProtocolMapperModel getProtocolMapperById(String id) {
+            return null;
+        }
+
+        @Override
+        public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
+            return null;
+        }
+
+        @Override
+        public boolean isFullScopeAllowed() {
+            return false;
+        }
+
+        @Override
+        public Set<RoleModel> getScopeMappings() {
+            RoleModel offlineAccess = realm.getRole("offline_access");
+            Set<RoleModel> set = new HashSet<>();
+            set.add(offlineAccess);
+            return set;
+        }
+
+        @Override
+        public Set<RoleModel> getRealmScopeMappings() {
+            return Collections.EMPTY_SET;
+        }
+
+        @Override
+        public boolean hasScope(RoleModel role) {
+            return false;
+        }
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java
new file mode 100644
index 0000000..67fcc0e
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientStorageProviderFactory.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.client.ClientStorageProviderFactory;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class HardcodedClientStorageProviderFactory implements ClientStorageProviderFactory<HardcodedClientStorageProvider> {
+    @Override
+    public HardcodedClientStorageProvider create(KeycloakSession session, ComponentModel model) {
+        return new HardcodedClientStorageProvider(session, new ClientStorageProviderModel(model));
+    }
+
+
+    public static final String PROVIDER_ID = "hardcoded-client";
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
+
+    public static final String CLIENT_ID = "client_id";
+
+    public static final String REDIRECT_URI = "redirect_uri";
+    public static final String CONSENT = "consent";
+
+    static {
+        CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
+                .property().name(CLIENT_ID)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .label("Hardcoded Client Id")
+                .helpText("Only this client id is available for lookup")
+                .defaultValue("hardcoded-client")
+                .add()
+                .property().name(REDIRECT_URI)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .label("Redirect Uri")
+                .helpText("Valid redirect uri.  Only one allowed")
+                .defaultValue("http://localhost:8180/*")
+                .add()
+                .property().name(CONSENT)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .label("Consent Required")
+                .helpText("Is consent required")
+                .defaultValue("false")
+                .add()
+                .build();
+    }
+
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory
new file mode 100644
index 0000000..0ed6376
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.client.ClientStorageProviderFactory
@@ -0,0 +1 @@
+org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
new file mode 100644
index 0000000..e251702
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2017 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.apache.commons.io.FileUtils;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.Details;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowBindings;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.cache.infinispan.ClientAdapter;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.storage.CacheableStorageProviderModel;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.client.ClientStorageProvider;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
+import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory;
+import org.keycloak.testsuite.federation.UserMapStorageFactory;
+import org.keycloak.testsuite.federation.UserPropertyFileStorageFactory;
+import org.keycloak.testsuite.forms.UsernameOnlyAuthenticator;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.TokenUtil;
+import org.openqa.selenium.By;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Calendar.DAY_OF_WEEK;
+import static java.util.Calendar.HOUR_OF_DAY;
+import static java.util.Calendar.MINUTE;
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.storage.CacheableStorageProviderModel.CACHE_POLICY;
+import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_DAY;
+import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_HOUR;
+import static org.keycloak.storage.CacheableStorageProviderModel.EVICTION_MINUTE;
+import static org.keycloak.storage.CacheableStorageProviderModel.MAX_LIFESPAN;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+
+/**
+ * Test that clients can override auth flows
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class ClientStorageTest extends AbstractTestRealmKeycloakTest {
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Page
+    protected AppPage appPage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Page
+    protected ErrorPage errorPage;
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+    }
+
+    protected String providerId;
+
+    @Deployment
+    public static WebArchive deploy() {
+        return RunOnServerDeployment.create(UserResource.class)
+                .addPackages(true, "org.keycloak.testsuite");
+    }
+
+    protected String addComponent(ComponentRepresentation component) {
+        Response resp = adminClient.realm("test").components().add(component);
+        resp.close();
+        String id = ApiUtil.getCreatedId(resp);
+        getCleanup().addComponentId(id);
+        return id;
+    }
+
+    @Before
+    public void addProvidersBeforeTest() throws URISyntaxException, IOException {
+        ComponentRepresentation provider = new ComponentRepresentation();
+        provider.setName("client-storage-hardcoded");
+        provider.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID);
+        provider.setProviderType(ClientStorageProvider.class.getName());
+        provider.setConfig(new MultivaluedHashMap<>());
+        provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client");
+        provider.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, oauth.getRedirectUri());
+
+        providerId = addComponent(provider);
+    }
+
+    protected String userId;
+
+    @Before
+    public void clientConfiguration() {
+        userId = findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId();
+        oauth.clientId("hardcoded-client");
+    }
+
+
+
+
+
+    @Test
+    public void testClientStats() throws Exception {
+        testDirectGrant("hardcoded-client");
+        testDirectGrant("hardcoded-client");
+        testBrowser("test-app");
+        offlineTokenDirectGrantFlowNoRefresh();
+        List<Map<String, String>> list = adminClient.realm("test").getClientSessionStats();
+        boolean hardTested = false;
+        boolean testAppTested = false;
+        for (Map<String, String> entry : list) {
+            if (entry.get("clientId").equals("hardcoded-client")) {
+                Assert.assertEquals("3", entry.get("active"));
+                Assert.assertEquals("1", entry.get("offline"));
+                hardTested = true;
+            } else if (entry.get("clientId").equals("test-app")) {
+                Assert.assertEquals("1", entry.get("active"));
+                Assert.assertEquals("0", entry.get("offline"));
+                testAppTested = true;
+            }
+        }
+        Assert.assertTrue(hardTested && testAppTested);
+    }
+
+
+    @Test
+    public void testBrowser() throws Exception {
+        String clientId = "hardcoded-client";
+        testBrowser(clientId);
+        //Thread.sleep(10000000);
+    }
+
+     private void testBrowser(String clientId) {
+        oauth.clientId(clientId);
+        String loginFormUrl = oauth.getLoginFormUrl();
+        //log.info("loginFormUrl: " + loginFormUrl);
+
+        //Thread.sleep(10000000);
+
+        driver.navigate().to(loginFormUrl);
+
+        loginPage.assertCurrent();
+
+        // Fill username+password. I am successfully authenticated
+        oauth.fillLoginForm("test-user@localhost", "password");
+        appPage.assertCurrent();
+
+        events.expectLogin().client(clientId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        Assert.assertNotNull(tokenResponse.getAccessToken());
+        Assert.assertNotNull(tokenResponse.getRefreshToken());
+
+        events.clear();
+
+    }
+
+    @Test
+    public void testGrantAccessTokenNoOverride() throws Exception {
+        testDirectGrant("hardcoded-client");
+    }
+
+    private void testDirectGrant(String clientId) {
+        Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
+        String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
+        WebTarget grantTarget = httpClient.target(grantUri);
+
+        {   // test no password
+            String header = BasicAuthHelper.createHeader(clientId, "password");
+            Form form = new Form();
+            form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
+            form.param("username", "test-user@localhost");
+            Response response = grantTarget.request()
+                    .header(HttpHeaders.AUTHORIZATION, header)
+                    .post(Entity.form(form));
+            assertEquals(401, response.getStatus());
+            response.close();
+        }
+
+        {   // test invalid password
+            String header = BasicAuthHelper.createHeader(clientId, "password");
+            Form form = new Form();
+            form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
+            form.param("username", "test-user@localhost");
+            form.param("password", "invalid");
+            Response response = grantTarget.request()
+                    .header(HttpHeaders.AUTHORIZATION, header)
+                    .post(Entity.form(form));
+            assertEquals(401, response.getStatus());
+            response.close();
+        }
+
+        {   // test valid password
+            String header = BasicAuthHelper.createHeader(clientId, "password");
+            Form form = new Form();
+            form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
+            form.param("username", "test-user@localhost");
+            form.param("password", "password");
+            Response response = grantTarget.request()
+                    .header(HttpHeaders.AUTHORIZATION, header)
+                    .post(Entity.form(form));
+            assertEquals(200, response.getStatus());
+            response.close();
+        }
+
+        httpClient.close();
+        events.clear();
+    }
+
+    @Test
+    public void testDailyEviction() {
+        testIsCached();
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientStorageProviderModel model = realm.getClientStorageProviders().get(0);
+            Calendar eviction = Calendar.getInstance();
+            eviction.add(Calendar.HOUR, 1);
+            model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_DAILY);
+            model.setEvictionHour(eviction.get(HOUR_OF_DAY));
+            model.setEvictionMinute(eviction.get(MINUTE));
+            realm.updateComponent(model);
+        });
+        testIsCached();
+        setTimeOffset(2 * 60 * 60); // 2 hours in future
+        testNotCached();
+        testIsCached();
+
+        setDefaultCachePolicy();
+        testIsCached();
+
+    }
+    @Test
+    public void testWeeklyEviction() {
+        testIsCached();
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientStorageProviderModel model = realm.getClientStorageProviders().get(0);
+            Calendar eviction = Calendar.getInstance();
+            eviction.add(Calendar.HOUR, 4 * 24);
+            model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.EVICT_WEEKLY);
+            model.setEvictionDay(eviction.get(DAY_OF_WEEK));
+            model.setEvictionHour(eviction.get(HOUR_OF_DAY));
+            model.setEvictionMinute(eviction.get(MINUTE));
+            realm.updateComponent(model);
+        });
+        testIsCached();
+        setTimeOffset(2 * 24 * 60 * 60); // 2 days in future
+        testIsCached();
+        setTimeOffset(5 * 24 * 60 * 60); // 5 days in future
+        testNotCached();
+        testIsCached();
+
+        setDefaultCachePolicy();
+        testIsCached();
+
+    }
+    @Test
+    public void testMaxLifespan() {
+        testIsCached();
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientStorageProviderModel model = realm.getClientStorageProviders().get(0);
+            model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.MAX_LIFESPAN);
+            model.setMaxLifespan(1 * 60 * 60 * 1000);
+            realm.updateComponent(model);
+        });
+        testIsCached();
+
+        setTimeOffset(1/2 * 60 * 60); // 1/2 hour in future
+
+        testIsCached();
+
+        setTimeOffset(2 * 60 * 60); // 2 hours in future
+
+        testNotCached();
+        testIsCached();
+
+        setDefaultCachePolicy();
+        testIsCached();
+
+    }
+
+    private void testNotCached() {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientModel hardcoded = realm.getClientByClientId("hardcoded-client");
+            Assert.assertNotNull(hardcoded);
+            Assert.assertFalse(hardcoded instanceof ClientAdapter);
+        });
+    }
+
+
+    @Test
+    public void testIsCached() {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientModel hardcoded = realm.getClientByClientId("hardcoded-client");
+            Assert.assertNotNull(hardcoded);
+            Assert.assertTrue(hardcoded instanceof org.keycloak.models.cache.infinispan.ClientAdapter);
+        });
+    }
+
+
+    @Test
+    public void testNoCache() {
+        testIsCached();
+
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientStorageProviderModel model = realm.getClientStorageProviders().get(0);
+            model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.NO_CACHE);
+            realm.updateComponent(model);
+        });
+
+        testNotCached();
+
+        // test twice because updating component should evict
+        testNotCached();
+
+        // set it back
+        setDefaultCachePolicy();
+        testIsCached();
+
+
+    }
+
+    private void setDefaultCachePolicy() {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ClientStorageProviderModel model = realm.getClientStorageProviders().get(0);
+            model.setCachePolicy(CacheableStorageProviderModel.CachePolicy.DEFAULT);
+            realm.updateComponent(model);
+        });
+    }
+
+    @Test
+    public void offlineTokenDirectGrantFlow() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("hardcoded-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
+        Assert.assertNull(tokenResponse.getErrorDescription());
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+
+        events.expectLogin()
+                .client("hardcoded-client")
+                .user(userId)
+                .session(token.getSessionState())
+                .detail(Details.GRANT_TYPE, OAuth2Constants.PASSWORD)
+                .detail(Details.TOKEN_ID, token.getId())
+                .detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .detail(Details.USERNAME, "test-user@localhost")
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+        Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
+        Assert.assertEquals(0, offlineToken.getExpiration());
+
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+
+        // Assert same token can be refreshed again
+        testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
+    }
+    public void offlineTokenDirectGrantFlowNoRefresh() throws Exception {
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        oauth.clientId("hardcoded-client");
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
+        Assert.assertNull(tokenResponse.getErrorDescription());
+        AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+        String offlineTokenString = tokenResponse.getRefreshToken();
+        RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
+    }
+
+    private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
+                                               final String sessionId, String userId) {
+        // Change offset to big value to ensure userSession expired
+        setTimeOffset(99999);
+        Assert.assertFalse(oldToken.isActive());
+        Assert.assertTrue(offlineToken.isActive());
+
+        // Assert userSession expired
+        testingClient.testing().removeExpired("test");
+        try {
+            testingClient.testing().removeUserSession("test", sessionId);
+        } catch (NotFoundException nfe) {
+            // Ignore
+        }
+
+        OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "password");
+        AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
+        Assert.assertEquals(200, response.getStatusCode());
+        Assert.assertEquals(sessionId, refreshedToken.getSessionState());
+
+        // Assert new refreshToken in the response
+        String newRefreshToken = response.getRefreshToken();
+        Assert.assertNotNull(newRefreshToken);
+        Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
+
+        Assert.assertEquals(userId, refreshedToken.getSubject());
+
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole(Constants.OFFLINE_ACCESS_ROLE));
+
+
+        EventRepresentation refreshEvent = events.expectRefresh(offlineToken.getId(), sessionId)
+                .client("hardcoded-client")
+                .user(userId)
+                .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
+                .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
+                .assertEvent();
+        Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
+
+        setTimeOffset(0);
+        return newRefreshToken;
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java
new file mode 100644
index 0000000..bf1e656
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/MapCollectTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.infinispan.stream.CacheCollectors;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MapCollectTest {
+
+    public static class UserSessionObject {
+        public String id;
+        public String realm;
+        public Set<String> clients = new HashSet<>();
+
+        public UserSessionObject(String realm, String... clients) {
+            this.id = UUID.randomUUID().toString();
+            this.realm = realm;
+            for (String c : clients) this.clients.add(c);
+        }
+    }
+
+    public static class RealmFilter implements Predicate<UserSessionObject> {
+        protected String realm;
+
+        public RealmFilter(String realm) {
+            this.realm = realm;
+        }
+
+        @Override
+        public boolean test(UserSessionObject entry) {
+            return entry.realm.equals(realm);
+        }
+
+        public static RealmFilter create(String realm) {
+            return new RealmFilter(realm);
+        }
+    }
+
+    public static Set<String> clients(UserSessionObject s) {
+        return s.clients;
+    }
+
+
+    @Test
+    public void testMe() throws Exception {
+
+        List<UserSessionObject> list = Arrays.asList(
+                new UserSessionObject("realm1", "a", "b")
+                , new UserSessionObject("realm1", "a", "c")
+                , new UserSessionObject("realm1", "a", "d")
+                , new UserSessionObject("realm1", "a", "b")
+                , new UserSessionObject("realm2", "a", "b")
+                , new UserSessionObject("realm2", "a", "c")
+                , new UserSessionObject("realm2", "a", "b")
+
+        );
+
+        Map<String, Long> result = list.stream().collect(
+                Collectors.groupingBy(s -> s.realm, Collectors.summingLong(i -> 1)));
+
+        for (Map.Entry<String, Long> entry : result.entrySet()) {
+            System.out.println(entry.getKey() + ":" + entry.getValue());
+        }
+
+        result = list.stream()
+                .filter(RealmFilter.create("realm1"))
+                .map(s->s.clients)
+                .flatMap(c->c.stream())
+                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
+
+        for (Map.Entry<String, Long> entry : result.entrySet()) {
+            System.out.println(entry.getKey() + ":" + entry.getValue());
+        }
+
+
+
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
index c3a341b..138ddcd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
@@ -41,7 +41,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.storage.UserStorageProvider;
 import static org.keycloak.storage.UserStorageProviderModel.CACHE_POLICY;
-import org.keycloak.storage.UserStorageProviderModel.CachePolicy;
+import org.keycloak.storage.CacheableStorageProviderModel.CachePolicy;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_DAY;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_HOUR;
 import static org.keycloak.storage.UserStorageProviderModel.EVICTION_MINUTE;
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
index 48228a2..d446985 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java
@@ -167,16 +167,36 @@ public class UserStorageTest {
         KeycloakSession session = keycloakRule.startSession();
         RealmModel realm = session.realms().getRealmByName("test");
         CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
-        long thorTimestamp = thor.getCacheTimestamp();
+        long lastTimestamp = thor.getCacheTimestamp();
         realm.updateComponent(model);
         keycloakRule.stopSession(session, true);
 
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        lastTimestamp = thor.getCacheTimestamp();
+        realm.updateComponent(model);
+        keycloakRule.stopSession(session, true);
+
+        // test is cached
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        // thor should be evicted because we changed the model
+        Assert.assertTrue(thor.getCacheTimestamp() > lastTimestamp);
+        lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+
         Time.setOffset(60 * 2 * 60); // 2 hours
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
         UserModel thor2 = session.users().getUserByUsername("thor", realm);
-        Assert.assertFalse(thor2 instanceof CachedUserModel);
+        // thor should be evicted because we put it 2 hours in the future
+        if (thor2 instanceof CachedUserModel) {
+            Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
+        }
         model.getConfig().remove("cachePolicy");
         model.getConfig().remove("evictionHour");
         model.getConfig().remove("evictionMinute");
@@ -199,24 +219,46 @@ public class UserStorageTest {
 
         KeycloakSession session = keycloakRule.startSession();
         RealmModel realm = session.realms().getRealmByName("test");
-        CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
         realm.updateComponent(model);
         keycloakRule.stopSession(session, true);
 
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        long lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
         Time.setOffset(60 * 60 * 24 * 2); // 2 days in future, should be cached still
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
         // test still
-        UserModel thor2 = session.users().getUserByUsername("thor", realm);
-        Assert.assertTrue(thor2 instanceof CachedUserModel);
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp);
+        lastTimestamp = thor.getCacheTimestamp();
         keycloakRule.stopSession(session, true);
+
         Time.setOffset(Time.getOffset() + 60 * 60 * 24 * 3); // 3 days into future, cache will be invalidated
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
-        thor2 = session.users().getUserByUsername("thor", realm);
-        Assert.assertFalse(thor2 instanceof CachedUserModel);
+        UserModel thor2 = session.users().getUserByUsername("thor", realm);
+        // thor should be evicted because we put it 2 hours in the future
+        if (thor2 instanceof CachedUserModel) {
+            Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
+        }
         model.getConfig().remove("cachePolicy");
         model.getConfig().remove("evictionHour");
         model.getConfig().remove("evictionMinute");
@@ -233,24 +275,44 @@ public class UserStorageTest {
 
         KeycloakSession session = keycloakRule.startSession();
         RealmModel realm = session.realms().getRealmByName("test");
-        CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
         realm.updateComponent(model);
         keycloakRule.stopSession(session, true);
 
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        CachedUserModel thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        long lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+
         Time.setOffset(60 * 5); // 5 minutes in future, should be cached still
+        session = keycloakRule.startSession();
+        realm = session.realms().getRealmByName("test");
+        thor = (CachedUserModel)session.users().getUserByUsername("thor", realm);
+        Assert.assertEquals(thor.getCacheTimestamp(), lastTimestamp);
+        lastTimestamp = thor.getCacheTimestamp();
+        keycloakRule.stopSession(session, true);
+
+        Time.setOffset(60 * 20); // 20 minutes into future, cache will be invalidated
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
-        // test still
         UserModel thor2 = session.users().getUserByUsername("thor", realm);
-        Assert.assertTrue(thor2 instanceof CachedUserModel);
+        // thor should be evicted because we put it 2 hours in the future
+        if (thor2 instanceof CachedUserModel) {
+            Assert.assertTrue(((CachedUserModel)thor2).getCacheTimestamp() > lastTimestamp);
+        }
         keycloakRule.stopSession(session, true);
-        Time.setOffset(60 * 20); // 20 minutes into future, cache will be invalidated
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("test");
-        thor2 = session.users().getUserByUsername("thor", realm);
-        Assert.assertFalse(thor2 instanceof CachedUserModel);
         model.getConfig().remove("cachePolicy");
         model.getConfig().remove("maxLifespan");
         realm.updateComponent(model);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java
index 9fe49f2..ced16a0 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentModelTest.java
@@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.ProtocolMapperModel;
@@ -30,6 +31,8 @@ import org.keycloak.models.UserConsentModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory;
 
 import java.util.List;
 
@@ -38,6 +41,8 @@ import java.util.List;
  */
 public class UserConsentModelTest extends AbstractModelTest {
 
+    private ComponentModel clientStorageComponent;
+
     @Before
     public void setupEnv() {
         RealmModel realm = realmManager.createRealm("original");
@@ -87,6 +92,22 @@ public class UserConsentModelTest extends AbstractModelTest {
         maryFooGrant.addGrantedProtocolMapper(fooMapper);
         realmManager.getSession().users().addConsent(realm, mary.getId(), maryFooGrant);
 
+        ClientStorageProviderModel clientStorage = new ClientStorageProviderModel();
+        clientStorage.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID);
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client");
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, "http://localhost:8081/*");
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CONSENT, "true");
+        clientStorage.setParentId(realm.getId());
+        clientStorageComponent = realm.addComponentModel(clientStorage);
+
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
+        Assert.assertNotNull(hardcodedClient);
+
+        UserConsentModel maryHardcodedGrant = new UserConsentModel(hardcodedClient);
+        realmManager.getSession().users().addConsent(realm, mary.getId(), maryHardcodedGrant);
+
+
         commit();
     }
 
@@ -125,7 +146,15 @@ public class UserConsentModelTest extends AbstractModelTest {
         Assert.assertNotNull("Created Date should be set", maryConsent.getCreatedDate());
         Assert.assertNotNull("Last Updated Date should be set", maryConsent.getLastUpdatedDate());
 
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        UserConsentModel maryHardcodedConsent = realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId());
+        Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0);
+        Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0);
+        Assert.assertNotNull("Created Date should be set", maryHardcodedConsent.getCreatedDate());
+        Assert.assertNotNull("Last Updated Date should be set", maryHardcodedConsent.getLastUpdatedDate());
+
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), barClient.getId()));
+        Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), hardcodedClient.getId()));
     }
 
     @Test
@@ -139,14 +168,26 @@ public class UserConsentModelTest extends AbstractModelTest {
         List<UserConsentModel> johnConsents = realmManager.getSession().users().getConsents(realm, john.getId());
         Assert.assertEquals(2, johnConsents.size());
 
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
         List<UserConsentModel> maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId());
-        Assert.assertEquals(1, maryConsents.size());
+        Assert.assertEquals(2, maryConsents.size());
         UserConsentModel maryConsent = maryConsents.get(0);
+        UserConsentModel maryHardcodedConsent = maryConsents.get(1);
+        if (maryConsents.get(0).getClient().getId().equals(hardcodedClient.getId())) {
+            maryConsent = maryConsents.get(1);
+            maryHardcodedConsent = maryConsents.get(0);
+
+        }
         Assert.assertEquals(maryConsent.getClient().getId(), fooClient.getId());
         Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
         Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
         Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
         Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+
+        Assert.assertEquals(maryHardcodedConsent.getClient().getId(), hardcodedClient.getId());
+        Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0);
+        Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0);
     }
 
     @Test
@@ -190,14 +231,19 @@ public class UserConsentModelTest extends AbstractModelTest {
         RealmModel realm = realmManager.getRealm("original");
         ClientModel fooClient = realm.getClientByClientId("foo-client");
         UserModel john = session.users().getUserByUsername("john", realm);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
 
         realmManager.getSession().users().revokeConsentForClient(realm, john.getId(), fooClient.getId());
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        realmManager.getSession().users().revokeConsentForClient(realm, mary.getId(), hardcodedClient.getId());
 
         commit();
 
         realm = realmManager.getRealm("original");
         john = session.users().getUserByUsername("john", realm);
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), fooClient.getId()));
+        mary = session.users().getUserByUsername("mary", realm);
+        Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId()));
     }
 
     @Test
@@ -206,6 +252,8 @@ public class UserConsentModelTest extends AbstractModelTest {
         RealmModel realm = realmManager.getRealm("original");
         UserModel john = session.users().getUserByUsername("john", realm);
         session.users().removeUser(realm, john);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+        session.users().removeUser(realm, mary);
     }
 
     @Test
@@ -270,6 +318,24 @@ public class UserConsentModelTest extends AbstractModelTest {
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), barClient.getId()));
     }
 
+    @Test
+    public void deleteClientStorageTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        realm.removeComponent(clientStorageComponent);
+        commit();
+
+
+
+        realm = realmManager.getRealm("original");
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        Assert.assertNull(hardcodedClient);
+
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+
+        List<UserConsentModel> maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId());
+        Assert.assertEquals(1, maryConsents.size());
+    }
+
     private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, UserConsentModel consentModel) {
         RoleModel role = roleContainer.getRole(roleName);
         return consentModel.isRoleGranted(role);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java
index 6fd18bf..04b5cde 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java
@@ -20,6 +20,7 @@ package org.keycloak.testsuite.model;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.ProtocolMapperModel;
@@ -31,6 +32,8 @@ import org.keycloak.models.UserModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
 import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.client.ClientStorageProviderModel;
+import org.keycloak.testsuite.federation.HardcodedClientStorageProviderFactory;
 import org.keycloak.testsuite.federation.storage.UserMapStorageFactory;
 import org.keycloak.testsuite.federation.storage.UserPropertyFileStorageFactory;
 
@@ -41,6 +44,8 @@ import java.util.List;
  */
 public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
 
+    private ComponentModel clientStorageComponent;
+
     @Before
     public void setupEnv() {
         RealmModel realm = realmManager.createRealm("original");
@@ -97,6 +102,22 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         maryFooGrant.addGrantedProtocolMapper(fooMapper);
         realmManager.getSession().users().addConsent(realm, mary.getId(), maryFooGrant);
 
+        ClientStorageProviderModel clientStorage = new ClientStorageProviderModel();
+        clientStorage.setProviderId(HardcodedClientStorageProviderFactory.PROVIDER_ID);
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CLIENT_ID, "hardcoded-client");
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.REDIRECT_URI, "http://localhost:8081/*");
+        clientStorage.getConfig().putSingle(HardcodedClientStorageProviderFactory.CONSENT, "true");
+        clientStorage.setParentId(realm.getId());
+        clientStorageComponent = realm.addComponentModel(clientStorage);
+
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
+        Assert.assertNotNull(hardcodedClient);
+
+        UserConsentModel maryHardcodedGrant = new UserConsentModel(hardcodedClient);
+        realmManager.getSession().users().addConsent(realm, mary.getId(), maryHardcodedGrant);
+
+
         commit();
     }
 
@@ -135,7 +156,15 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         Assert.assertNotNull("Created Date should be set", maryConsent.getCreatedDate());
         Assert.assertNotNull("Last Updated Date should be set", maryConsent.getLastUpdatedDate());
 
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        UserConsentModel maryHardcodedConsent = realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId());
+        Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0);
+        Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0);
+        Assert.assertNotNull("Created Date should be set", maryHardcodedConsent.getCreatedDate());
+        Assert.assertNotNull("Last Updated Date should be set", maryHardcodedConsent.getLastUpdatedDate());
+
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), barClient.getId()));
+        Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), hardcodedClient.getId()));
     }
 
     @Test
@@ -149,14 +178,26 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         List<UserConsentModel> johnConsents = realmManager.getSession().users().getConsents(realm, john.getId());
         Assert.assertEquals(2, johnConsents.size());
 
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+
         List<UserConsentModel> maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId());
-        Assert.assertEquals(1, maryConsents.size());
+        Assert.assertEquals(2, maryConsents.size());
         UserConsentModel maryConsent = maryConsents.get(0);
+        UserConsentModel maryHardcodedConsent = maryConsents.get(1);
+        if (maryConsents.get(0).getClient().getId().equals(hardcodedClient.getId())) {
+            maryConsent = maryConsents.get(1);
+            maryHardcodedConsent = maryConsents.get(0);
+
+        }
         Assert.assertEquals(maryConsent.getClient().getId(), fooClient.getId());
         Assert.assertEquals(maryConsent.getGrantedRoles().size(), 1);
         Assert.assertEquals(maryConsent.getGrantedProtocolMappers().size(), 1);
         Assert.assertTrue(isRoleGranted(realm, "realm-role", maryConsent));
         Assert.assertTrue(isMapperGranted(fooClient, "foo", maryConsent));
+
+        Assert.assertEquals(maryHardcodedConsent.getClient().getId(), hardcodedClient.getId());
+        Assert.assertEquals(maryHardcodedConsent.getGrantedRoles().size(), 0);
+        Assert.assertEquals(maryHardcodedConsent.getGrantedProtocolMappers().size(), 0);
     }
 
     @Test
@@ -200,14 +241,19 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         RealmModel realm = realmManager.getRealm("original");
         ClientModel fooClient = realm.getClientByClientId("foo-client");
         UserModel john = session.users().getUserByUsername("john", realm);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
 
         realmManager.getSession().users().revokeConsentForClient(realm, john.getId(), fooClient.getId());
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        realmManager.getSession().users().revokeConsentForClient(realm, mary.getId(), hardcodedClient.getId());
 
         commit();
 
         realm = realmManager.getRealm("original");
         john = session.users().getUserByUsername("john", realm);
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), fooClient.getId()));
+        mary = session.users().getUserByUsername("mary", realm);
+        Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, mary.getId(), hardcodedClient.getId()));
     }
 
     @Test
@@ -216,6 +262,8 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         RealmModel realm = realmManager.getRealm("original");
         UserModel john = session.users().getUserByUsername("john", realm);
         session.users().removeUser(realm, john);
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+        session.users().removeUser(realm, mary);
     }
 
     @Test
@@ -280,6 +328,24 @@ public class UserConsentWithUserStorageModelTest extends AbstractModelTest {
         Assert.assertNull(realmManager.getSession().users().getConsentByClient(realm, john.getId(), barClient.getId()));
     }
 
+    @Test
+    public void deleteClientStorageTest() {
+        RealmModel realm = realmManager.getRealm("original");
+        realm.removeComponent(clientStorageComponent);
+        commit();
+
+
+
+        realm = realmManager.getRealm("original");
+        ClientModel hardcodedClient = session.realms().getClientByClientId("hardcoded-client", realm);
+        Assert.assertNull(hardcodedClient);
+
+        UserModel mary = session.users().getUserByUsername("mary", realm);
+
+        List<UserConsentModel> maryConsents = realmManager.getSession().users().getConsents(realm, mary.getId());
+        Assert.assertEquals(1, maryConsents.size());
+    }
+
     private boolean isRoleGranted(RoleContainerModel roleContainer, String roleName, UserConsentModel consentModel) {
         RoleModel role = roleContainer.getRole(roleName);
         return consentModel.isRoleGranted(role);
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 3595dd9..f8ff2d0 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
@@ -173,6 +173,7 @@ realm-sessions=Realm Sessions
 revocation=Revocation
 logout-all=Logout all
 active-sessions=Active Sessions
+offline-sessions=Offline Sessions
 sessions=Sessions
 not-before=Not Before
 not-before.tooltip=Revoke any tokens issued before this date.
@@ -1343,6 +1344,8 @@ userStorage.cachePolicy.maxLifespan.tooltip=Max lifespan of a user cache entry i
 user-origin-link=Storage Origin
 user-origin.tooltip=UserStorageProvider the user was loaded from
 user-link.tooltip=UserStorageProvider this locally stored user was imported from.
+client-origin-link=Storage Origin
+client-origin.tooltip=Provider the client was loaded from
 
 disable=Disable
 disableable-credential-types=Disableable Types
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 2e88f02..1625218 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -764,6 +764,15 @@ module.controller('ClientListCtrl', function($scope, realm, Client, serverInfo, 
         });
     };
 
+    $scope.searchClient = function() {
+        console.log('searchQuery!!! ' + $scope.search.clientId);
+        Client.query({realm: realm.realm, viewableOnly: true, clientId: $scope.search.clientId}).$promise.then(function(clients) {
+            $scope.numberOfPages = Math.ceil(clients.length/$scope.pageSize);
+            $scope.clients = clients;
+        });
+
+    };
+
     $scope.exportClient = function(client) {
         var clientCopy = angular.copy(client);
         delete clientCopy.id;
@@ -819,7 +828,7 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, serv
     }
 });
 
-module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
+module.controller('ClientDetailCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, Components, ClientStorageOperations, $location, $modal, Dialog, Notifications) {
 
 
 
@@ -889,6 +898,25 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
     $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
     $scope.disableCredentialsTab = client.publicClient;
 
+    if(client.origin) {
+        if ($scope.access.viewRealm) {
+            Components.get({realm: realm.realm, componentId: client.origin}, function (link) {
+                $scope.originName = link.name;
+                //$scope.originLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
+            })
+        }
+        else {
+            // KEYCLOAK-4328
+            ClientStorageOperations.simpleName.get({realm: realm.realm, componentId: client.origin}, function (link) {
+                $scope.originName = link.name;
+                //$scope.originLink = $location.absUrl();
+            })
+        }
+    } else {
+        console.log("origin is null");
+    }
+
+
     function updateProperties() {
         if (!$scope.client.attributes) {
             $scope.client.attributes = {};
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index b1b6304..5700792 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1813,6 +1813,16 @@ module.factory('UserStorageOperations', function($resource) {
 });
 
 
+module.factory('ClientStorageOperations', function($resource) {
+    var object = {}
+    object.simpleName = $resource(authUrl + '/admin/realms/:realm/client-storage/:componentId/name', {
+        realm : '@realm',
+        componentId : '@componentId'
+    });
+    return object;
+});
+
+
 module.factory('ClientRegistrationPolicyProviders', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/client-registration-policy/providers', {
         realm : '@realm',
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index c00601a..cbeb321 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -37,6 +37,13 @@
                 </div>
                 <kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix block" data-ng-show="client.origin">
+                <label class="col-md-2 control-label">{{:: 'client-origin-link' | translate}}</label>
+                <div class="col-md-6">
+                    {{originName}}
+                </div>
+                <kc-tooltip>{{:: 'client-origin.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
                 <label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
                 <div class="col-sm-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
index 03ebf5c..744e3c4 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-list.html
@@ -11,9 +11,9 @@
                     <div class="form-inline">
                         <div class="form-group">
                             <div class="input-group">
-                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeyup="if(event.keyCode === 13){$(this).next('I').click();}">
+                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.clientId" class="form-control search" onkeydown="if(event.keyCode === 13) document.getElementById('clientSearch').click()">
                                 <div class="input-group-addon">
-                                    <i class="fa fa-search" type="submit"></i>
+                                    <i class="fa fa-search" id="clientSearch" data-ng-click="searchClient()"></i>
                                 </div>
                             </div>
                         </div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
index e724d95..98b2284 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html
@@ -18,12 +18,14 @@
         <tr>
             <th>{{:: 'client' | translate}}</th>
             <th>{{:: 'active-sessions' | translate}}</th>
+            <th>{{:: 'offline-sessions' | translate}}</th>
         </tr>
         </thead>
         <tbody>
         <tr data-ng-repeat="data in stats">
             <td><a href="#/realms/{{realm.realm}}/clients/{{data.id}}/sessions">{{data.clientId}}</a></td>
             <td>{{data.active}}</td>
+            <td>{{data.offline}}</td>
         </tr>
         </tbody>
     </table>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 40aab54..c294692 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -9,11 +9,11 @@
     <ul class="nav nav-tabs"  data-ng-hide="create && !path[4]">
         <li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'credentials'}"
-            data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
+            data-ng-show="!client.publicClient && client.protocol == 'openid-connect' && !client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
         </li>
         <li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
-        <li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'roles'}" data-ng-show="!client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/mappers">{{:: 'mappers' | translate}}</a>
             <kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
@@ -23,10 +23,10 @@
             <kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
         </li>
         <li ng-class="{active: path[4] == 'authz'}"
-            data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled">
+            data-ng-show="serverInfo.featureEnabled('AUTHORIZATION') && !disableAuthorizationTab && client.authorizationServicesEnabled && !client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
                 translate}}</a></li>
-        <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml'"><a
+        <li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2' && client.protocol != 'saml' && !client.origin"><a
                 href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
         </li>
     <!--    <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
@@ -40,9 +40,9 @@
             <kc-tooltip>{{:: 'offline-access.tooltip' | translate}}</kc-tooltip>
         </li>
 
-        <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient && !client.origin"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
 
-        <li ng-class="{active: path[4] == 'installation'}">
+        <li ng-class="{active: path[4] == 'installation'}" data-ng-show="!client.origin">
             <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">{{:: 'installation' | translate}}</a>
             <kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
         </li>