keycloak-aplcache

Changes

Details

diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index f800ae2..7f18d0d 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -91,6 +91,8 @@
                 <local-cache name="sessions"/>
                 <local-cache name="authenticationSessions"/>
                 <local-cache name="offlineSessions"/>
+                <local-cache name="clientSessions"/>
+                <local-cache name="offlineClientSessions"/>
                 <local-cache name="loginFailures"/>
                 <local-cache name="authorization">
                     <eviction max-entries="10000" strategy="LRU"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
index 4dd4e56..9bfd8dc 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli
@@ -398,4 +398,16 @@ if (outcome == success) of /profile=$clusteredProfile/subsystem=jsf/:read-resour
     end-try
 end-if
 
+if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:read-resource
+  echo Adding distributed-cache=offlineClientSessions to keycloak cache container...
+  /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:add(mode=SYNC,owners=1)
+  echo
+end-if
+
+if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:read-resource
+  echo Adding distributed-cache=clientSessions to keycloak cache container...
+  /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:add(mode=SYNC,owners=1)
+  echo
+end-if
+
 echo *** End Migration of /profile=$clusteredProfile ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
index 100808a..1632413 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli
@@ -361,4 +361,16 @@ if (outcome == success) of /profile=$standaloneProfile/subsystem=jsf/:read-resou
     end-try
 end-if
 
+if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:read-resource
+  echo Adding local-cache=clientSessions to keycloak cache container...
+  /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:add(indexing=NONE,start=LAZY)
+  echo
+end-if
+
+if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:read-resource
+  echo Adding local-cache=offlineClientSessions to keycloak cache container...
+  /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:add(indexing=NONE,start=LAZY)
+  echo
+end-if
+
 echo *** End Migration of /profile=$standaloneProfile ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
index e348149..c9d0cab 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli
@@ -350,4 +350,16 @@ if (outcome == success) of /subsystem=jsf/:read-resource
     echo
 end-if
 
+if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:read-resource
+  echo Adding local-cache=offlineClientSessions to keycloak cache container...
+  /subsystem=infinispan/cache-container=keycloak/local-cache=offlineClientSessions/:add(indexing=NONE,start=LAZY)
+  echo
+end-if
+
+if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:read-resource
+  echo Adding local-cache=clientSessions to keycloak cache container...
+  /subsystem=infinispan/cache-container=keycloak/local-cache=clientSessions/:add(indexing=NONE,start=LAZY)
+  echo
+end-if
+
 echo *** End Migration ***
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
index 50e51e6..918ed35 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli
@@ -382,4 +382,16 @@ if (outcome == success) of /subsystem=jsf/:read-resource
     echo
 end-if
 
+if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:read-resource
+  echo Adding distributed-cache=clientSessions to keycloak cache container...
+  /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/:add(mode=SYNC,owners=1)
+  echo
+end-if
+
+if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:read-resource
+  echo Adding distributed-cache=offlineClientSessions to keycloak cache container...
+  /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/:add(mode=SYNC,owners=1)
+  echo
+end-if
+
 echo *** End Migration ***
\ No newline at end of file
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
index 6bb11d5..d4b02f8 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
@@ -9,6 +9,8 @@ embed-server --server-config=standalone-ha.xml
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add(mode="SYNC",owners="1")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
 /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
 /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
diff --git a/misc/CrossDataCenter.md b/misc/CrossDataCenter.md
index c24a890..fa69f2d 100644
--- a/misc/CrossDataCenter.md
+++ b/misc/CrossDataCenter.md
@@ -188,7 +188,8 @@ Keycloak servers setup
 </distributed-cache>
 ```
 
-3.6) Same for `offlineSessions`, `loginFailures`, and `actionTokens` caches (the only difference from `sessions` cache is that `cache` property value are different):
+3.6) Same for `offlineSessions`, `clientSessions`, `offlineClientSessions`, `loginFailures`, and `actionTokens` caches (the only difference
+from `sessions` cache is that `cache` property value are different):
 
 ```xml
 <distributed-cache name="offlineSessions" mode="SYNC" owners="1">
@@ -198,6 +199,20 @@ Keycloak servers setup
     </remote-store>
 </distributed-cache>
 
+<distributed-cache name="clientSessions" mode="SYNC" owners="1">
+    <remote-store cache="clientSessions" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
+        <property name="rawValues">true</property>
+        <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
+    </remote-store>
+</distributed-cache>
+
+<distributed-cache name="offlineClientSessions" mode="SYNC" owners="1">
+    <remote-store cache="offlineClientSessions" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
+        <property name="rawValues">true</property>
+        <property name="marshaller">org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory</property>
+    </remote-store>
+</distributed-cache>
+
 <distributed-cache name="loginFailures" mode="SYNC" owners="1">
     <remote-store cache="loginFailures" remote-servers="remote-cache" passivation="false" fetch-state="false" purge="false" preload="false" shared="true">
         <property name="rawValues">true</property>
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index c657e44..572c5f0 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -245,18 +245,34 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         if (jdgEnabled) {
             sessionConfigBuilder = new ConfigurationBuilder();
             sessionConfigBuilder.read(sessionCacheConfigurationBase);
-            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
+            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, true);
         }
         Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, sessionCacheConfiguration);
 
         if (jdgEnabled) {
             sessionConfigBuilder = new ConfigurationBuilder();
             sessionConfigBuilder.read(sessionCacheConfigurationBase);
-            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true);
+            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true);
         }
         sessionCacheConfiguration = sessionConfigBuilder.build();
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, sessionCacheConfiguration);
+
+        if (jdgEnabled) {
+            sessionConfigBuilder = new ConfigurationBuilder();
+            sessionConfigBuilder.read(sessionCacheConfigurationBase);
+            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, true);
+        }
+        sessionCacheConfiguration = sessionConfigBuilder.build();
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, sessionCacheConfiguration);
+
+        if (jdgEnabled) {
+            sessionConfigBuilder = new ConfigurationBuilder();
+            sessionConfigBuilder.read(sessionCacheConfigurationBase);
+            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, true);
+        }
+        sessionCacheConfiguration = sessionConfigBuilder.build();
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, sessionCacheConfiguration);
 
         if (jdgEnabled) {
             sessionConfigBuilder = new ConfigurationBuilder();
@@ -269,8 +285,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase);
 
         // Retrieve caches to enforce rebalance
-        cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
-        cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true);
+        cacheManager.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, true);
+        cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true);
+        cacheManager.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, true);
+        cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, true);
         cacheManager.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, true);
         cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
 
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 9c3d437..00f60a7 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -33,8 +33,10 @@ public interface InfinispanConnectionProvider extends Provider {
     String USER_REVISIONS_CACHE_NAME = "userRevisions";
     int USER_REVISIONS_CACHE_DEFAULT_MAX = 100000;
 
-    String SESSION_CACHE_NAME = "sessions";
-    String OFFLINE_SESSION_CACHE_NAME = "offlineSessions";
+    String USER_SESSION_CACHE_NAME = "sessions";
+    String CLIENT_SESSION_CACHE_NAME = "clientSessions";
+    String OFFLINE_USER_SESSION_CACHE_NAME = "offlineSessions";
+    String OFFLINE_CLIENT_SESSION_CACHE_NAME = "offlineClientSessions";
     String LOGIN_FAILURE_CACHE_NAME = "loginFailures";
     String AUTHENTICATION_SESSIONS_CACHE_NAME = "authenticationSessions";
     String WORK_CACHE_NAME = "work";
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
index a786084..736e756 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
@@ -28,10 +28,15 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
-import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus;
+import org.keycloak.models.sessions.infinispan.changes.Tasks;
 import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import java.util.UUID;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -40,56 +45,48 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     private AuthenticatedClientSessionEntity entity;
     private final ClientModel client;
-    private final InfinispanUserSessionProvider provider;
-    private final InfinispanChangelogBasedTransaction updateTx;
-    private UserSessionAdapter userSession;
+    private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
+    private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
+    private UserSessionModel userSession;
+
+    public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client,
+                                             UserSessionModel userSession,
+                                             InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
+                                             InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
+        if (userSession == null) {
+            throw new NullPointerException("userSession must not be null");
+        }
 
-    public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession,
-                                             InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) {
-        this.provider = provider;
         this.entity = entity;
-        this.client = client;
-        this.updateTx = updateTx;
         this.userSession = userSession;
+        this.client = client;
+        this.userSessionUpdateTx = userSessionUpdateTx;
+        this.clientSessionUpdateTx = clientSessionUpdateTx;
     }
 
     private void update(UserSessionUpdateTask task) {
-        updateTx.addTask(userSession.getId(), task);
+        userSessionUpdateTx.addTask(userSession.getId(), task);
     }
 
+    private void update(ClientSessionUpdateTask task) {
+        clientSessionUpdateTx.addTask(entity.getId(), task);
+    }
 
+    /**
+     * Detaches the client session from its user session.
+     * <p>
+     * <b>This method does not delete the client session from user session records, it only removes the client session.</b>
+     * The list of client sessions within user session is updated lazily for performance reasons.
+     */
     @Override
-    public void setUserSession(UserSessionModel userSession) {
-        String clientUUID = client.getId();
+    public void detachFromUserSession() {
+        // Intentionally do not remove the clientUUID from the user session, invalid session is handled
+        // as nonexistent in org.keycloak.models.sessions.infinispan.UserSessionAdapter.getAuthenticatedClientSessions()
+        this.userSession = null;
 
-        // Dettach userSession
-        if (userSession == null) {
-            UserSessionUpdateTask task = new UserSessionUpdateTask() {
-
-                @Override
-                public void runUpdate(UserSessionEntity sessionEntity) {
-                    sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
-                }
-
-            };
-            update(task);
-            this.userSession = null;
-        } else {
-            this.userSession = (UserSessionAdapter) userSession;
-            UserSessionUpdateTask task = new UserSessionUpdateTask() {
-
-                @Override
-                public void runUpdate(UserSessionEntity sessionEntity) {
-                    AuthenticatedClientSessionEntity current = sessionEntity.getAuthenticatedClientSessions().putIfAbsent(clientUUID, entity);
-                    if (current != null) {
-                        // It may happen when 2 concurrent HTTP requests trying SSO login against same client
-                        entity = current;
-                    }
-                }
-
-            };
-            update(task);
-        }
+        SessionUpdateTask<AuthenticatedClientSessionEntity> removeTask = Tasks.removeSync();
+
+        clientSessionUpdateTx.addTask(entity.getId(), removeTask);
     }
 
     @Override
@@ -104,10 +101,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setRedirectUri(String uri) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setRedirectUri(uri);
             }
 
@@ -138,19 +135,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setTimestamp(int timestamp) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setTimestamp(timestamp);
             }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                // We usually update lastSessionRefresh at the same time. That would handle it.
-                return CrossDCMessageStatus.NOT_NEEDED;
-            }
-
         };
 
         update(task);
@@ -163,19 +153,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
             }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                // We usually update lastSessionRefresh at the same time. That would handle it.
-                return CrossDCMessageStatus.NOT_NEEDED;
-            }
-
         };
 
         update(task);
@@ -188,19 +171,12 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setCurrentRefreshToken(String currentRefreshToken) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setCurrentRefreshToken(currentRefreshToken);
             }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                // We usually update lastSessionRefresh at the same time. That would handle it.
-                return CrossDCMessageStatus.NOT_NEEDED;
-            }
-
         };
 
         update(task);
@@ -213,10 +189,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setAction(String action) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setAction(action);
             }
 
@@ -232,10 +208,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setProtocol(String method) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setAuthMethod(method);
             }
 
@@ -251,10 +227,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setRoles(Set<String> roles) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...?
             }
 
@@ -270,10 +246,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setProtocolMappers(Set<String> protocolMappers) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...?
             }
 
@@ -289,10 +265,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void setNote(String name, String value) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.getNotes().put(name, value);
             }
 
@@ -303,10 +279,10 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
 
     @Override
     public void removeNote(String name) {
-        UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+        ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
 
             @Override
-            protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+            public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.getNotes().remove(name);
             }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
index 694c41e..2a0b436 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
@@ -45,9 +45,9 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
 
     private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
 
-    public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
+    public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
         this.kcSession = kcSession;
-        this.cacheName = cacheName;
+        this.cacheName = cache.getName();
         this.cache = cache;
         this.remoteCacheInvoker = remoteCacheInvoker;
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java
new file mode 100644
index 0000000..214e2e9
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/Tasks.java
@@ -0,0 +1,80 @@
+/*
+ * 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.models.sessions.infinispan.changes;
+
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class Tasks {
+
+    private static final SessionUpdateTask<? extends SessionEntity> ADD_IF_ABSENT_SYNC = new SessionUpdateTask<SessionEntity>() {
+        @Override
+        public void runUpdate(SessionEntity entity) {
+        }
+
+        @Override
+        public CacheOperation getOperation(SessionEntity entity) {
+            return CacheOperation.ADD_IF_ABSENT;
+        }
+
+        @Override
+        public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<SessionEntity> sessionWrapper) {
+            return CrossDCMessageStatus.SYNC;
+        }
+    };
+
+    private static final SessionUpdateTask<? extends SessionEntity> REMOVE_SYNC = new SessionUpdateTask<SessionEntity>() {
+        @Override
+        public void runUpdate(SessionEntity entity) {
+        }
+
+        @Override
+        public CacheOperation getOperation(SessionEntity entity) {
+            return CacheOperation.REMOVE;
+        }
+
+        @Override
+        public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<SessionEntity> sessionWrapper) {
+            return CrossDCMessageStatus.SYNC;
+        }
+    };
+
+    /**
+     * Returns a typed task of type {@link CacheOperation#ADD_IF_ABSENT} that does no other update. This operation has DC message
+     * status {@link CrossDCMessageStatus#SYNC}.
+     * @param <S>
+     * @return
+     */
+    public static <S extends SessionEntity> SessionUpdateTask<S> addIfAbsentSync() {
+        return (SessionUpdateTask<S>) ADD_IF_ABSENT_SYNC;
+    }
+
+    /**
+     * Returns a typed task of type {@link CacheOperation#REMOVE} that does no other update. This operation has DC message
+     * status {@link CrossDCMessageStatus#SYNC}.
+     * @param <S>
+     * @return
+     */
+    public static <S extends SessionEntity> SessionUpdateTask<S> removeSync() {
+        return (SessionUpdateTask<S>) REMOVE_SYNC;
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
index b8a6223..898648a 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java
@@ -20,7 +20,6 @@ package org.keycloak.models.sessions.infinispan.entities;
 import java.io.IOException;
 import java.io.ObjectInput;
 import java.io.ObjectOutput;
-import java.io.Serializable;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -29,13 +28,14 @@ import org.infinispan.commons.marshall.Externalizer;
 import org.infinispan.commons.marshall.MarshallUtil;
 import org.infinispan.commons.marshall.SerializeWith;
 import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
+import java.util.UUID;
 
 /**
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class)
-public class AuthenticatedClientSessionEntity implements Serializable {
+public class AuthenticatedClientSessionEntity extends SessionEntity {
 
     private String authMethod;
     private String redirectUri;
@@ -49,6 +49,16 @@ public class AuthenticatedClientSessionEntity implements Serializable {
     private String currentRefreshToken;
     private int currentRefreshTokenUseCount;
 
+    private final UUID id;
+
+    private AuthenticatedClientSessionEntity(UUID id) {
+        this.id = id;
+    }
+
+    public AuthenticatedClientSessionEntity() {
+        this.id = UUID.randomUUID();
+    }
+
     public String getAuthMethod() {
         return authMethod;
     }
@@ -121,10 +131,16 @@ public class AuthenticatedClientSessionEntity implements Serializable {
         this.currentRefreshTokenUseCount = currentRefreshTokenUseCount;
     }
 
+    public UUID getId() {
+        return id;
+    }
+
     public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
 
         @Override
         public void writeObject(ObjectOutput output, AuthenticatedClientSessionEntity session) throws IOException {
+            MarshallUtil.marshallUUID(session.id, output, false);
+            MarshallUtil.marshallString(session.getRealmId(), output);
             MarshallUtil.marshallString(session.getAuthMethod(), output);
             MarshallUtil.marshallString(session.getRedirectUri(), output);
             MarshallUtil.marshallInt(output, session.getTimestamp());
@@ -143,7 +159,9 @@ public class AuthenticatedClientSessionEntity implements Serializable {
 
         @Override
         public AuthenticatedClientSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
-            AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity();
+            AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity(MarshallUtil.unmarshallUUID(input, false));
+
+            sessionEntity.setRealmId(MarshallUtil.unmarshallString(input));
 
             sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
             sessionEntity.setRedirectUri(MarshallUtil.unmarshallString(input));
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java
new file mode 100644
index 0000000..ebf946e
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionStore.java
@@ -0,0 +1,115 @@
+/*
+ * 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.models.sessions.infinispan.entities;
+
+import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.SerializeWith;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@SerializeWith(AuthenticatedClientSessionStore.ExternalizerImpl.class)
+public class AuthenticatedClientSessionStore {
+
+    /**
+     * Maps client UUID to client session ID.
+     */
+    private final ConcurrentHashMap<String, UUID> authenticatedClientSessionIds;
+
+    public AuthenticatedClientSessionStore() {
+        authenticatedClientSessionIds = new ConcurrentHashMap<>();
+    }
+
+    private AuthenticatedClientSessionStore(ConcurrentHashMap<String, UUID> authenticatedClientSessionIds) {
+        this.authenticatedClientSessionIds = authenticatedClientSessionIds;
+    }
+
+    public void clear() {
+        authenticatedClientSessionIds.clear();
+    }
+
+    public boolean containsKey(String key) {
+        return authenticatedClientSessionIds.containsKey(key);
+    }
+
+    public void forEach(BiConsumer<? super String, ? super UUID> action) {
+        authenticatedClientSessionIds.forEach(action);
+    }
+
+    public UUID get(String key) {
+        return authenticatedClientSessionIds.get(key);
+    }
+
+    public Set<String> keySet() {
+        return authenticatedClientSessionIds.keySet();
+    }
+
+    public UUID put(String key, UUID value) {
+        return authenticatedClientSessionIds.put(key, value);
+    }
+
+    public UUID remove(String clientUUID) {
+        return authenticatedClientSessionIds.remove(clientUUID);
+    }
+
+    public int size() {
+        return authenticatedClientSessionIds.size();
+    }
+
+    @Override
+    public String toString() {
+        return this.authenticatedClientSessionIds.toString();
+    }
+
+    public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionStore> {
+
+        private static final int VERSION_1 = 1;
+
+        @Override
+        public void writeObject(ObjectOutput output, AuthenticatedClientSessionStore obj) throws IOException {
+            output.writeByte(VERSION_1);
+
+            KeycloakMarshallUtil.writeMap(obj.authenticatedClientSessionIds, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.UUID_EXT, output);
+        }
+
+        @Override
+        public AuthenticatedClientSessionStore readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+            switch (input.readByte()) {
+                case VERSION_1:
+                    return readObjectVersion1(input);
+                default:
+                    throw new IOException("Unknown version");
+            }
+        }
+
+        public AuthenticatedClientSessionStore readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
+            AuthenticatedClientSessionStore res = new AuthenticatedClientSessionStore(
+              KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.UUID_EXT, ConcurrentHashMap::new)
+            );
+            return res;
+        }
+    }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index 78df451..dbde092 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -78,7 +78,7 @@ public class UserSessionEntity extends SessionEntity {
 
     private Map<String, String> notes = new ConcurrentHashMap<>();
 
-    private Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions  = new ConcurrentHashMap<>();
+    private AuthenticatedClientSessionStore authenticatedClientSessions = new AuthenticatedClientSessionStore();
 
     public String getUser() {
         return user;
@@ -144,11 +144,11 @@ public class UserSessionEntity extends SessionEntity {
         this.notes = notes;
     }
 
-    public Map<String, AuthenticatedClientSessionEntity> getAuthenticatedClientSessions() {
+    public AuthenticatedClientSessionStore getAuthenticatedClientSessions() {
         return authenticatedClientSessions;
     }
 
-    public void setAuthenticatedClientSessions(Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions) {
+    public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) {
         this.authenticatedClientSessions = authenticatedClientSessions;
     }
 
@@ -264,8 +264,7 @@ public class UserSessionEntity extends SessionEntity {
             Map<String, String> notes = session.getNotes();
             KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
 
-            Map<String, AuthenticatedClientSessionEntity> authSessions = session.getAuthenticatedClientSessions();
-            KeycloakMarshallUtil.writeMap(authSessions, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), output);
+            output.writeObject(session.getAuthenticatedClientSessions());
         }
 
 
@@ -285,7 +284,8 @@ public class UserSessionEntity extends SessionEntity {
             sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
             sessionEntity.setBrokerSessionId(MarshallUtil.unmarshallString(input));
             sessionEntity.setBrokerUserId(MarshallUtil.unmarshallString(input));
-            sessionEntity.setId(MarshallUtil.unmarshallString(input));
+            final String userSessionId = MarshallUtil.unmarshallString(input);
+            sessionEntity.setId(userSessionId);
             sessionEntity.setIpAddress(MarshallUtil.unmarshallString(input));
             sessionEntity.setLoginUsername(MarshallUtil.unmarshallString(input));
             sessionEntity.setRealmId(MarshallUtil.unmarshallString(input));
@@ -301,8 +301,7 @@ public class UserSessionEntity extends SessionEntity {
                     new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
             sessionEntity.setNotes(notes);
 
-            Map<String, AuthenticatedClientSessionEntity> authSessions = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(),
-                    new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
+            AuthenticatedClientSessionStore authSessions = (AuthenticatedClientSessionStore) input.readObject();
             sessionEntity.setAuthenticatedClientSessions(authSessions);
 
             return sessionEntity;
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 8b6df90..2c0411e 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
@@ -23,7 +23,6 @@ import org.infinispan.context.Flag;
 import org.jboss.logging.Logger;
 import org.keycloak.cluster.ClusterProvider;
 import org.keycloak.common.util.Time;
-import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
@@ -33,12 +32,16 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.UserSessionProvider;
 import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.models.sessions.infinispan.changes.Tasks;
 import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
 import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
 import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation;
+import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
@@ -58,8 +61,10 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+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.Predicate;
 import java.util.stream.Stream;
@@ -75,10 +80,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache;
     protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
+    protected final Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache;
+    protected final Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache;
     protected final Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache;
 
     protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
     protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
+    protected final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionTx;
+    protected final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> offlineClientSessionTx;
     protected final InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> loginFailuresTx;
 
     protected final SessionEventsSenderTransaction clusterEventsSenderTx;
@@ -92,17 +101,23 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                                          LastSessionRefreshStore offlineLastSessionRefreshStore,
                                          Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
                                          Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
+                                         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache,
+                                         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionCache,
                                          Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache) {
         this.session = session;
 
         this.sessionCache = sessionCache;
+        this.clientSessionCache = clientSessionCache;
         this.offlineSessionCache = offlineSessionCache;
+        this.offlineClientSessionCache = offlineClientSessionCache;
         this.loginFailureCache = loginFailureCache;
 
-        this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker);
-        this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker);
+        this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, remoteCacheInvoker);
+        this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, remoteCacheInvoker);
+        this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, remoteCacheInvoker);
+        this.offlineClientSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineClientSessionCache, remoteCacheInvoker);
 
-        this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, loginFailureCache, remoteCacheInvoker);
+        this.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, loginFailureCache, remoteCacheInvoker);
 
         this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
 
@@ -112,6 +127,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
         session.getTransactionManager().enlistAfterCompletion(sessionTx);
         session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
+        session.getTransactionManager().enlistAfterCompletion(clientSessionTx);
+        session.getTransactionManager().enlistAfterCompletion(offlineClientSessionTx);
         session.getTransactionManager().enlistAfterCompletion(loginFailuresTx);
     }
 
@@ -123,6 +140,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         return offline ? offlineSessionTx : sessionTx;
     }
 
+    protected Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> getClientSessionCache(boolean offline) {
+        return offline ? offlineClientSessionCache : clientSessionCache;
+    }
+
+    protected InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> getClientSessionTransaction(boolean offline) {
+        return offline ? offlineClientSessionTx : clientSessionTx;
+    }
+
     protected LastSessionRefreshStore getLastSessionRefreshStore() {
         return lastSessionRefreshStore;
     }
@@ -134,10 +159,20 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     @Override
     public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
         AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
+        entity.setRealmId(realm.getId());
+        final UUID clientSessionId = entity.getId();
+
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
+        AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession,
+          userSessionUpdateTx, clientSessionUpdateTx);
+
+        SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
+        clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity);
+
+        SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId);
+        userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask);
 
-        InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx = getTransaction(false);
-        AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
-        adapter.setUserSession(userSession);
         return adapter;
     }
 
@@ -147,25 +182,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setId(id);
         updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
 
-        SessionUpdateTask<UserSessionEntity> createSessionTask = new SessionUpdateTask<UserSessionEntity>() {
-
-            @Override
-            public void runUpdate(UserSessionEntity session) {
-
-            }
-
-            @Override
-            public CacheOperation getOperation(UserSessionEntity session) {
-                return CacheOperation.ADD_IF_ABSENT;
-            }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
-
-        };
-
+        SessionUpdateTask<UserSessionEntity> createSessionTask = Tasks.addIfAbsentSync();
         sessionTx.addTask(id, createSessionTask, entity);
 
         return wrap(realm, entity, false);
@@ -229,6 +246,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     @Override
+    public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline) {
+        AuthenticatedClientSessionEntity entity = getClientSessionEntity(clientSessionId, offline);
+        return wrap(userSession, client, entity, offline);
+    }
+
+    private AuthenticatedClientSessionEntity getClientSessionEntity(UUID id, boolean offline) {
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> tx = getClientSessionTransaction(offline);
+        SessionEntityWrapper<AuthenticatedClientSessionEntity> entityWrapper = tx.get(id);
+        return entityWrapper == null ? null : entityWrapper.getEntity();
+    }
+
+
+    @Override
     public List<UserSessionModel> getUserSessions(final RealmModel realm, UserModel user) {
         return getUserSessions(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), false);
     }
@@ -256,12 +286,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
         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(client.getId()))
+                .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid))
                 .map(Mappers.userSessionEntity())
+                // Filter out client sessions that have been invalidated in the meantime
+                .filter(userSession -> {
+                    final UUID clientSessionId = userSession.getAuthenticatedClientSessions().get(clientUuid);
+                    return clientSessionId != null && clientSessionCacheDecorated.containsKey(clientSessionId);
+                })
                 .sorted(Comparators.userSessionLastSessionRefresh());
 
         if (firstResult > 0) {
@@ -356,8 +395,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         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();
+
         return cache.entrySet().stream()
-                .filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
+                .filter(UserSessionPredicate.create(realm.getId()).client(clientUuid))
+                // Filter out client sessions that have been invalidated in the meantime
+                .map(Mappers.userSessionEntity())
+                .filter(userSession -> {
+                    final UUID clientSessionId = userSession.getAuthenticatedClientSessions().get(clientUuid);
+                    return clientSessionId != null && clientSessionCacheDecorated.containsKey(clientSessionId);
+                })
                 .count();
     }
 
@@ -402,28 +452,38 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         // Each cluster node cleanups just local sessions, which are those owned by itself (+ few more taking l1 cache into account)
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache);
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
+        final AtomicInteger userSessionsSize = new AtomicInteger();
+
         // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
         localCacheStoreIgnore
                 .entrySet()
                 .stream()
                 .filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh))
-                .map(Mappers.sessionId())
-                .forEach(new Consumer<String>() {
+                .map(Mappers.userSessionEntity())
+                .forEach(new Consumer<UserSessionEntity>() {
 
                     @Override
-                    public void accept(String sessionId) {
-                        Future future = localCache.removeAsync(sessionId);
+                    public void accept(UserSessionEntity userSessionEntity) {
+                        userSessionsSize.incrementAndGet();
+
+                        Future future = localCache.removeAsync(userSessionEntity.getId());
                         futures.addTask(future);
+
+                        userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
+                            Future f = localClientSessionCache.removeAsync(clientSessionId);
+                            futures.addTask(f);
+                        });
                     }
 
                 });
 
         futures.waitForAllToFinish();
 
-        log.debugf("Removed %d expired user sessions for realm '%s'", futures.size(), realm.getName());
+        log.debugf("Removed %d expired user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
     }
 
     private void removeExpiredOfflineUserSessions(RealmModel realm) {
@@ -432,6 +492,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache);
 
         UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
 
@@ -439,6 +500,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
+        final AtomicInteger userSessionsSize = new AtomicInteger();
+
         // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
         localCacheStoreIgnore
                 .entrySet()
@@ -449,8 +512,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
                     @Override
                     public void accept(UserSessionEntity userSessionEntity) {
+                        userSessionsSize.incrementAndGet();
+
                         Future future = localCache.removeAsync(userSessionEntity.getId());
                         futures.addTask(future);
+                        userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
+                            Future f = localClientSessionCache.removeAsync(clientSessionId);
+                            futures.addTask(f);
+                        });
 
                         // TODO:mposolda can be likely optimized to delete all expired at one step
                         persister.removeUserSession( userSessionEntity.getId(), true);
@@ -464,7 +533,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         futures.waitForAllToFinish();
 
-        log.debugf("Removed %d expired offline user sessions for realm '%s'", futures.size(), realm.getName());
+        log.debugf("Removed %d expired offline user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
     }
 
     @Override
@@ -484,28 +553,39 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(cache);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = getClientSessionCache(offline);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(clientSessionCache);
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
+        final AtomicInteger userSessionsSize = new AtomicInteger();
+
         localCacheStoreIgnore
                 .entrySet()
                 .stream()
                 .filter(SessionPredicate.create(realmId))
-                .map(Mappers.sessionId())
-                .forEach(new Consumer<String>() {
+                .map(Mappers.userSessionEntity())
+                .forEach(new Consumer<UserSessionEntity>() {
 
                     @Override
-                    public void accept(String sessionId) {
+                    public void accept(UserSessionEntity userSessionEntity) {
+                        userSessionsSize.incrementAndGet();
+
                         // Remove session from remoteCache too. Use removeAsync for better perf
-                        Future future = localCache.removeAsync(sessionId);
+                        Future future = localCache.removeAsync(userSessionEntity.getId());
                         futures.addTask(future);
+                        userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
+                            Future f = localClientSessionCache.removeAsync(clientSessionId);
+                            futures.addTask(f);
+                        });
                     }
 
                 });
 
+
         futures.waitForAllToFinish();
 
-        log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) futures.size(), realmId, offline);
+        log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline);
     }
 
     @Override
@@ -528,25 +608,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setRealmId(realm.getId());
         entity.setUserId(userId);
 
-        SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = new SessionUpdateTask<LoginFailureEntity>() {
-
-            @Override
-            public void runUpdate(LoginFailureEntity session) {
-
-            }
-
-            @Override
-            public CacheOperation getOperation(LoginFailureEntity session) {
-                return CacheOperation.ADD_IF_ABSENT;
-            }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
-
-        };
-
+        SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = Tasks.addIfAbsentSync();
         loginFailuresTx.addTask(key, createLoginFailureTask, entity);
 
         return wrap(key, entity);
@@ -554,25 +616,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     @Override
     public void removeUserLoginFailure(RealmModel realm, String userId) {
-        SessionUpdateTask<LoginFailureEntity> removeTask = new SessionUpdateTask<LoginFailureEntity>() {
-
-            @Override
-            public void runUpdate(LoginFailureEntity entity) {
-
-            }
-
-            @Override
-            public CacheOperation getOperation(LoginFailureEntity entity) {
-                return CacheOperation.REMOVE;
-            }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
-
-        };
-
+        SessionUpdateTask<LoginFailureEntity> removeTask = Tasks.removeSync();
         loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask);
     }
 
@@ -648,28 +692,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
-        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
-
-        SessionUpdateTask<UserSessionEntity> removeTask = new SessionUpdateTask<UserSessionEntity>() {
-
-            @Override
-            public void runUpdate(UserSessionEntity entity) {
-
-            }
-
-            @Override
-            public CacheOperation getOperation(UserSessionEntity entity) {
-                return CacheOperation.REMOVE;
-            }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
-
-        };
-
-        tx.addTask(sessionEntity.getId(), removeTask);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
+        sessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync()));
+        SessionUpdateTask<UserSessionEntity> removeTask = Tasks.removeSync();
+        userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask);
     }
 
     InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> getLoginFailuresTx() {
@@ -677,8 +704,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
-        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
-        return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
+        return entity != null ? new UserSessionAdapter(session, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline) : null;
+    }
+
+    AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
+        return entity != null ? new AuthenticatedClientSessionAdapter(entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx) : null;
     }
 
     UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@@ -726,7 +760,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession :
                 getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId());
 
-        AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true));
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(true);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(true);
+        AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
 
         // update timestamp to current time
         offlineClientSession.setTimestamp(Time.currentTime());
@@ -776,7 +812,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setIpAddress(userSession.getIpAddress());
         entity.setLoginUsername(userSession.getLoginUsername());
         entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes());
-        entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>());
+        entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore());
         entity.setRememberMe(userSession.isRememberMe());
         entity.setState(userSession.getState());
         entity.setUser(userSession.getUser().getId());
@@ -784,35 +820,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setStarted(userSession.getStarted());
         entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
 
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
 
-        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
-
-        SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
-
-            @Override
-            public void runUpdate(UserSessionEntity session) {
-
-            }
-
-            @Override
-            public CacheOperation getOperation(UserSessionEntity session) {
-                return CacheOperation.ADD_IF_ABSENT;
-            }
-
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
-
-        };
-        tx.addTask(userSession.getId(), importTask, entity);
+        SessionUpdateTask<UserSessionEntity> importTask = Tasks.addIfAbsentSync();
+        userSessionUpdateTx.addTask(userSession.getId(), importTask, entity);
 
         UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
 
         // Handle client sessions
         if (importAuthenticatedClientSessions) {
             for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
-                importClientSession(importedSession, clientSession, tx);
+                importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
             }
         }
 
@@ -820,9 +839,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
 
-    private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession,
-                                                                  InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx) {
+    private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession,
+                                                                  InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
+                                                                  InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
         AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
+        entity.setRealmId(sessionToImportInto.getRealm().getId());
+        final UUID clientSessionId = entity.getId();
 
         entity.setAction(clientSession.getAction());
         entity.setAuthMethod(clientSession.getProtocol());
@@ -833,33 +855,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setRoles(clientSession.getRoles());
         entity.setTimestamp(clientSession.getTimestamp());
 
+        SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
+        clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity);
 
-        Map<String, AuthenticatedClientSessionEntity> clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions();
+        AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions();
+        clientSessions.put(clientSession.getClient().getId(), clientSessionId);
 
-        clientSessions.put(clientSession.getClient().getId(), entity);
+        SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
+        userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
 
-        SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
+        return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx);
+    }
 
-            @Override
-            public void runUpdate(UserSessionEntity session) {
-                Map<String, AuthenticatedClientSessionEntity> clientSessions = session.getAuthenticatedClientSessions();
-                clientSessions.put(clientSession.getClient().getId(), entity);
-            }
+    private static class RegisterClientSessionTask implements SessionUpdateTask<UserSessionEntity> {
 
-            @Override
-            public CacheOperation getOperation(UserSessionEntity session) {
-                return CacheOperation.REPLACE;
-            }
+        private final String clientUuid;
+        private final UUID clientSessionId;
 
-            @Override
-            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
-                return CrossDCMessageStatus.SYNC;
-            }
+        public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) {
+            this.clientUuid = clientUuid;
+            this.clientSessionId = clientSessionId;
+        }
+
+        @Override
+        public void runUpdate(UserSessionEntity session) {
+            AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions();
+            clientSessions.put(clientUuid, clientSessionId);
+        }
 
-        };
-        updateTx.addTask(importedUserSession.getId(), importTask);
+        @Override
+        public CacheOperation getOperation(UserSessionEntity session) {
+            return CacheOperation.REPLACE;
+        }
 
-        return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx);
+        @Override
+        public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
+            return CrossDCMessageStatus.SYNC;
+        }
     }
 
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index ccc9b84..ae2b3d4 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -37,6 +37,7 @@ import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer;
 import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer;
 import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
@@ -58,6 +59,7 @@ import org.keycloak.provider.ProviderEventListener;
 
 import java.io.Serializable;
 import java.util.Set;
+import java.util.UUID;
 
 public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
 
@@ -82,11 +84,14 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
     @Override
     public InfinispanUserSessionProvider create(KeycloakSession session) {
         InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionCache = connections.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
         Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
 
-        return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures);
+        return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, 
+          cache, offlineSessionsCache, clientSessionCache, offlineClientSessionsCache, loginFailures);
     }
 
     @Override
@@ -205,7 +210,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
 
         InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
 
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
         boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
             return realm.getSsoSessionIdleTimeout() * 1000;
         });
@@ -214,8 +219,12 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
             lastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
         }
 
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
+        checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
+            return realm.getSsoSessionIdleTimeout() * 1000;
+        });
 
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
         boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
             return realm.getOfflineSessionIdleTimeout() * 1000;
         });
@@ -224,8 +233,13 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
             offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
         }
 
+        Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> offlineClientSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME);
+        checkRemoteCache(session, offlineClientSessionsCache, (RealmModel realm) -> {
+            return realm.getOfflineSessionIdleTimeout() * 1000;
+        });
+
         Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
-        boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
+        checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
             return realm.getMaxDeltaTimeSeconds() * 1000;
         });
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
index b96b9bd..128a7e9 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
@@ -142,7 +142,7 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
                 .getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
                 .get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC);
 
-        if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) {
+        if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) {
             log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName);
             return true;
         } else {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
index d240b13..0b9a764 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
@@ -17,8 +17,8 @@
 
 package org.keycloak.models.sessions.infinispan.stream;
 
+import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
 import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
@@ -54,6 +54,11 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
         this.realm = realm;
     }
 
+    /**
+     * Creates a user session predicate. If using the {@link #client(java.lang.String)} method, see its warning.
+     * @param realm
+     * @return
+     */
     public static UserSessionPredicate create(String realm) {
         return new UserSessionPredicate(realm);
     }
@@ -63,6 +68,15 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
         return this;
     }
 
+    /**
+     * Adds a test for client. Note that this test can return stale sessions because on detaching client session
+     * from user session, only client session is deleted and user session is not updated for performance reason.
+     *
+     * @see AuthenticatedClientSessionAdapter#detachFromUserSession()
+     * @param clientSessionCache
+     * @param clientUUID
+     * @return
+     */
     public UserSessionPredicate client(String clientUUID) {
         this.client = clientUUID;
         return this;
@@ -86,9 +100,7 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
 
     @Override
     public boolean test(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
-        SessionEntity e = entry.getValue().getEntity();
-
-        UserSessionEntity entity = (UserSessionEntity) e;
+        UserSessionEntity entity = entry.getValue().getEntity();
 
         if (!realm.equals(entity.getRealmId())) {
             return false;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index 635d4f7..de82557 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -26,15 +26,22 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
 import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.changes.Tasks;
 import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -45,7 +52,9 @@ public class UserSessionAdapter implements UserSessionModel {
 
     private final InfinispanUserSessionProvider provider;
 
-    private final InfinispanChangelogBasedTransaction updateTx;
+    private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
+
+    private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
 
     private final RealmModel realm;
 
@@ -53,11 +62,14 @@ public class UserSessionAdapter implements UserSessionModel {
 
     private final boolean offline;
 
-    public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm,
-                              UserSessionEntity entity, boolean offline) {
+    public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, 
+                              InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
+                              InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
+                              RealmModel realm, UserSessionEntity entity, boolean offline) {
         this.session = session;
         this.provider = provider;
-        this.updateTx = updateTx;
+        this.userSessionUpdateTx = userSessionUpdateTx;
+        this.clientSessionUpdateTx = clientSessionUpdateTx;
         this.realm = realm;
         this.entity = entity;
         this.offline = offline;
@@ -65,17 +77,20 @@ public class UserSessionAdapter implements UserSessionModel {
 
     @Override
     public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
-        Map<String, AuthenticatedClientSessionEntity> clientSessionEntities = entity.getAuthenticatedClientSessions();
+        AuthenticatedClientSessionStore clientSessionEntities = entity.getAuthenticatedClientSessions();
         Map<String, AuthenticatedClientSessionModel> result = new HashMap<>();
 
         List<String> removedClientUUIDS = new LinkedList<>();
 
         if (clientSessionEntities != null) {
-            clientSessionEntities.forEach((String key, AuthenticatedClientSessionEntity value) -> {
+            clientSessionEntities.forEach((String key, UUID value) -> {
                 // Check if client still exists
                 ClientModel client = realm.getClientById(key);
                 if (client != null) {
-                    result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx));
+                    final AuthenticatedClientSessionAdapter clientSession = provider.getClientSession(this, client, value, offline);
+                    if (clientSession != null) {
+                        result.put(key, clientSession);
+                    }
                 } else {
                     removedClientUUIDS.add(key);
                 }
@@ -88,20 +103,53 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
-    public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
-        if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
+    public AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) {
+        AuthenticatedClientSessionStore clientSessionEntities = entity.getAuthenticatedClientSessions();
+        final UUID clientSessionId = clientSessionEntities.get(clientUUID);
+
+        if (clientSessionId == null) {
+            return null;
+        }
+
+        ClientModel client = realm.getClientById(clientUUID);
+
+        if (client != null) {
+            return provider.getClientSession(this, client, clientSessionId, offline);
+        }
+
+        removeAuthenticatedClientSessions(Collections.singleton(clientUUID));
+        return null;
+    }
+
+    private static final int MINIMUM_INACTIVE_CLIENT_SESSIONS_TO_CLEANUP = 5;
+
+    @Override
+    public void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS) {
+        if (removedClientUUIDS == null || ! removedClientUUIDS.isEmpty()) {
             return;
         }
 
-        // Update user session
-        UserSessionUpdateTask task = new UserSessionUpdateTask() {
-            @Override
-            public void runUpdate(UserSessionEntity entity) {
-                removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove);
-            }
-        };
+        // Performance: do not remove the clientUUIDs from the user session until there is enough of them;
+        // an invalid session is handled as nonexistent in UserSessionAdapter.getAuthenticatedClientSessions()
+        if (removedClientUUIDS.size() >= MINIMUM_INACTIVE_CLIENT_SESSIONS_TO_CLEANUP) {
+            // Update user session
+            UserSessionUpdateTask task = new UserSessionUpdateTask() {
+                @Override		
+                public void runUpdate(UserSessionEntity entity) {		
+                    removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove);		
+                }		
+            };		
+            update(task);
+        }
 
-        update(task);
+        // do not iterate the removedClientUUIDS and remove the clientSession directly as the addTask can manipulate
+        // the collection being iterated, and that can lead to unpredictable behaviour (e.g. NPE)
+        List<UUID> clientSessionUuids = removedClientUUIDS.stream()
+          .map(entity.getAuthenticatedClientSessions()::get)
+          .filter(Objects::nonNull)
+          .collect(Collectors.toList());
+
+        clientSessionUuids.forEach(clientSessionId -> this.clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync()));
     }
 
     public String getId() {
@@ -276,7 +324,7 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     void update(UserSessionUpdateTask task) {
-        updateTx.addTask(getId(), task);
+        userSessionUpdateTx.addTask(getId(), task);
     }
 
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java
index df5c8c6..a1d8366 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/KeycloakMarshallUtil.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.infinispan.commons.marshall.Externalizer;
@@ -41,7 +42,19 @@ public class KeycloakMarshallUtil {
 
     private static final Logger log = Logger.getLogger(KeycloakMarshallUtil.class);
 
-    public static final StringExternalizer STRING_EXT = new StringExternalizer();
+    public static final Externalizer<String> STRING_EXT = new StringExternalizer();
+
+    public static final Externalizer<UUID> UUID_EXT = new Externalizer<UUID>() {
+        @Override
+        public void writeObject(ObjectOutput output, UUID uuid) throws IOException {
+            MarshallUtil.marshallUUID(uuid, output, true);
+        }
+
+        @Override
+        public UUID readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+            return MarshallUtil.unmarshallUUID(input, true);
+        }
+    };
 
     // MAP
 
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
index ca06914..fb0394a 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoteCacheTest.java
@@ -77,8 +77,8 @@ public class ConcurrencyJDGRemoteCacheTest {
     }
 
     private static Worker createWorker(int threadId) {
-        EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
-        Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        EmbeddedCacheManager manager = new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
+        Cache<String, Integer> cache = manager.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
 
         System.out.println("Retrieved cache: " + threadId);
 
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java
index 5b7abe0..536ee33 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGRemoveSessionTest.java
@@ -41,6 +41,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import java.util.UUID;
 
 /**
  * Check that removing of session from remoteCache is session immediately removed on remoteCache in other DC. This is true.
@@ -68,9 +69,11 @@ public class ConcurrencyJDGRemoveSessionTest {
 
     //private static Map<String, EntryInfo> state = new HashMap<>();
 
+    private static final UUID CLIENT_1_UUID = UUID.randomUUID();
+
     public static void main(String[] args) throws Exception {
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
 
         // Create caches, listeners and finally worker threads
         Thread worker1 = createWorker(cache1, 1);
@@ -177,7 +180,7 @@ public class ConcurrencyJDGRemoveSessionTest {
         clientSession.setTimestamp(1234);
         clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
         clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
-        session.getAuthenticatedClientSessions().put("client1", clientSession);
+        session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId());
 
         SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
         return wrappedSession;
@@ -205,7 +208,7 @@ public class ConcurrencyJDGRemoveSessionTest {
 
 
     private static EmbeddedCacheManager createManager(int threadId) {
-        return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
+        return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
     }
 
 
diff --git a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
index 2b175ba..2dfb508 100644
--- a/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java
@@ -36,7 +36,6 @@ import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
 import org.infinispan.context.Flag;
 import org.infinispan.manager.EmbeddedCacheManager;
 import org.jboss.logging.Logger;
-import org.junit.Assert;
 import org.keycloak.common.util.Time;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
@@ -44,6 +43,7 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import java.util.UUID;
 import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
 
 /**
@@ -74,9 +74,11 @@ public class ConcurrencyJDGSessionsCacheTest {
 
     //private static Map<String, EntryInfo> state = new HashMap<>();
 
+    private static final UUID CLIENT_1_UUID = UUID.randomUUID();
+
     public static void main(String[] args) throws Exception {
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
 
         // Create initial item
         UserSessionEntity session = new UserSessionEntity();
@@ -96,7 +98,7 @@ public class ConcurrencyJDGSessionsCacheTest {
         clientSession.setTimestamp(1234);
         clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
         clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
-        session.getAuthenticatedClientSessions().put("client1", clientSession);
+        session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId());
 
         SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
 
@@ -219,7 +221,7 @@ public class ConcurrencyJDGSessionsCacheTest {
 
 
     private static EmbeddedCacheManager createManager(int threadId) {
-        return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
+        return new TestCacheManagerFactory().createManager(threadId, InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
     }
 
 
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java
index a5aae29..a9bb784 100644
--- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheConcurrentWritesTest.java
@@ -38,6 +38,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import java.util.UUID;
 
 /**
  * Test concurrent writes to distributed cache with usage of atomic replace
@@ -52,6 +53,8 @@ public class DistributedCacheConcurrentWritesTest {
     private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
     private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
 
+    private static final UUID CLIENT_1_UUID = UUID.randomUUID();
+
     public static void main(String[] args) throws Exception {
         CacheWrapper<String, UserSessionEntity> cache1 = createCache("node1");
         CacheWrapper<String, UserSessionEntity> cache2 = createCache("node2");
@@ -74,7 +77,7 @@ public class DistributedCacheConcurrentWritesTest {
         clientSession.setTimestamp(1234);
         clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
         clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
-        session.getAuthenticatedClientSessions().put("client1", clientSession);
+        session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId());
 
         cache1.put("123", session);
 
@@ -211,7 +214,7 @@ public class DistributedCacheConcurrentWritesTest {
 
     public static CacheWrapper<String, UserSessionEntity> createCache(String nodeName) {
         EmbeddedCacheManager mgr = createManager(nodeName);
-        Cache<String, SessionEntityWrapper<UserSessionEntity>> wrapped = mgr.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        Cache<String, SessionEntityWrapper<UserSessionEntity>> wrapped = mgr.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
         return new CacheWrapper<>(wrapped);
     }
 
@@ -245,7 +248,7 @@ public class DistributedCacheConcurrentWritesTest {
         }
         Configuration distConfig = distConfigBuilder.build();
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, distConfig);
         return cacheManager;
 
     }
diff --git a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java
index 2ea5245..d6bc350 100644
--- a/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/DistributedCacheWriteSkewTest.java
@@ -35,27 +35,29 @@ import org.infinispan.transaction.LockingMode;
 import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
 import org.infinispan.util.concurrent.IsolationLevel;
 import org.jgroups.JChannel;
-import org.junit.Ignore;
 import org.keycloak.common.util.Time;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import java.util.UUID;
 
 /**
  * Test concurrent writes to distributed cache with usage of write skew
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-@Ignore
+//@Ignore
 public class DistributedCacheWriteSkewTest {
 
     private static final int ITERATION_PER_WORKER = 1000;
 
     private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
 
+    private static final UUID CLIENT_1_UUID = UUID.randomUUID();
+
     public static void main(String[] args) throws Exception {
-        Cache<String, UserSessionEntity> cache1 = createManager("node1").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
-        Cache<String, UserSessionEntity> cache2 = createManager("node2").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+        Cache<String, UserSessionEntity> cache1 = createManager("node1").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
+        Cache<String, UserSessionEntity> cache2 = createManager("node2").getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
 
         // Create initial item
         UserSessionEntity session = new UserSessionEntity();
@@ -75,7 +77,7 @@ public class DistributedCacheWriteSkewTest {
         clientSession.setTimestamp(1234);
         clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
         clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
-        session.getAuthenticatedClientSessions().put("client1", clientSession);
+        session.getAuthenticatedClientSessions().put(CLIENT_1_UUID.toString(), clientSession.getId());
 
         cache1.put("123", session);
 
@@ -149,6 +151,7 @@ public class DistributedCacheWriteSkewTest {
                         replaced = true;
                     } catch (Exception e) {
                         System.out.println(e);
+                        e.printStackTrace();
                         failedReplaceCounter.incrementAndGet();
                     }
 
@@ -208,7 +211,7 @@ public class DistributedCacheWriteSkewTest {
         }
         Configuration distConfig = distConfigBuilder.build();
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, distConfig);
         return cacheManager;
 
     }
diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
index cee10e1..c54533c 100644
--- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
@@ -27,7 +27,10 @@ import org.keycloak.sessions.CommonClientSessionModel;
  */
 public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
 
-    void setUserSession(UserSessionModel userSession);
+    /**
+     * Detaches the client session from its user session.
+     */
+    void detachFromUserSession();
     UserSessionModel getUserSession();
 
     String getCurrentRefreshToken();
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
index ff7c864..40fdada 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models;
 
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -53,15 +54,22 @@ public interface UserSessionModel {
     void setLastSessionRefresh(int seconds);
 
     /**
-     * Returns map where key is ID of the client (its UUID) and value is the respective {@link AuthenticatedClientSessionModel} object.
+     * Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object.
      * @return 
      */
     Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
     /**
+     * Returns a client session for the given client UUID.
+     * @return
+     */
+    default AuthenticatedClientSessionModel getAuthenticatedClientSessionByClient(String clientUUID) {
+        return getAuthenticatedClientSessions().get(clientUUID);
+    };
+    /**
      * Removes authenticated client sessions for all clients whose UUID is present in {@code removedClientUUIDS} parameter.
      * @param removedClientUUIDS
      */
-    void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS);
+    void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS);
 
 
     public String getNote(String name);
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 8334838..cd265f2 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.UUID;
 import java.util.function.Predicate;
 
 /**
@@ -29,6 +30,7 @@ import java.util.function.Predicate;
 public interface UserSessionProvider extends Provider {
 
     AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession);
+    AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline);
 
     UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
     UserSessionModel getUserSession(RealmModel realm, String id);
@@ -39,7 +41,7 @@ public interface UserSessionProvider extends Provider {
     UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId);
 
     /**
-     * Return userSession of specified ID as long as the predicate passes. Otherwise returs null.
+     * Return userSession of specified ID as long as the predicate passes. Otherwise returns {@code null}.
      * If predicate doesn't pass, implementation can do some best-effort actions to try have predicate passing (eg. download userSession from other DC)
      */
     UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java
index 670e9cb..f38b31c 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java
@@ -116,6 +116,10 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
     }
 
     @Override
+    public void detachFromUserSession() {
+        setUserSession(null);
+    }
+
     public void setUserSession(UserSessionModel userSession) {
         this.userSession = userSession;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
index e3b5777..095a857 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
@@ -26,6 +26,7 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.util.JsonSerialization;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -161,7 +162,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
     }
 
     @Override
-    public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
+    public void removeAuthenticatedClientSessions(Collection<String> removedClientUUIDS) {
         if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
             return;
         }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index cf85aa4..562a208 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -254,7 +254,7 @@ public class TokenEndpoint {
 
             // Attempt to use same code twice should invalidate existing clientSession
             if (clientSession != null) {
-                clientSession.setUserSession(null);
+                clientSession.detachFromUserSession();
             }
 
             event.error(Errors.INVALID_CODE);
@@ -400,7 +400,7 @@ public class TokenEndpoint {
 
             if (!result.isOfflineToken()) {
                 UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
-                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
                 updateClientSession(clientSession);
                 updateUserSessionFromClientAuth(userSession);
             }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index 9571fde..0c913d0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -166,7 +166,7 @@ public class UserInfoEndpoint {
 
 
         // Existence of authenticatedClientSession for our client already handled before
-        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId());
+        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
 
         AccessToken userInfo = new AccessToken();
         tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 4a08614..d0774c4 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -159,13 +159,13 @@ public class TokenManager {
         }
 
         ClientModel client = session.getContext().getClient();
-        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
 
         // Can theoretically happen in cross-dc environment. Try to see if userSession with our client is available in remoteCache
         if (clientSession == null) {
             userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, userSession.getId(), offline, client.getId());
             if (userSession != null) {
-                clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+                clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
             } else {
                 throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session doesn't have required client", "Session doesn't have required client");
             }
@@ -400,7 +400,7 @@ public class TokenManager {
     public static AuthenticatedClientSessionModel attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) {
         ClientModel client = authSession.getClient();
 
-        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
         if (clientSession == null) {
             clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
         }
@@ -436,8 +436,9 @@ public class TokenManager {
             return;
         }
 
-        clientSession.setUserSession(null);
+        clientSession.detachFromUserSession();
 
+        // TODO: Might need optimization to prevent loading client sessions from cache in getAuthenticatedClientSessions()
         if (userSession.getAuthenticatedClientSessions().isEmpty()) {
             sessions.removeUserSession(realm, userSession);
         }
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index afea781..ec2fa26 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -400,7 +400,7 @@ public class SamlService extends AuthorizationEndpointBase {
                 userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
                 userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
                 // remove client from logout requests
-                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
                 if (clientSession != null) {
                     clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
                 }
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
index 83b54dd..f9354b4 100644
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
@@ -62,7 +62,7 @@ public class SamlSessionUtils {
             return null;
         }
 
-        return userSession.getAuthenticatedClientSessions().get(parts[1]);
+        return userSession.getAuthenticatedClientSessionByClient(clientUUID);
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 7c77955..1e8a53d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -395,7 +395,7 @@ public class AuthenticationManager {
     public static void backchannelLogoutUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
         List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
         for (UserSessionModel userSession : userSessions) {
-            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
             if (clientSession != null) {
                 AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers);
                 clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
index 70e3f35..de141ff 100644
--- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
+++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
@@ -172,7 +172,7 @@ class CodeGenerateUtil {
                 }
             }
 
-            return userSession.getAuthenticatedClientSessions().get(codeJWT.getIssuedFor());
+            return userSession.getAuthenticatedClientSessionByClient(codeJWT.getIssuedFor());
 
         }
 
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java
index de2516f..476022c 100644
--- a/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionCrossDCManager.java
@@ -42,7 +42,7 @@ public class UserSessionCrossDCManager {
 
     // get userSession if it has "authenticatedClientSession" of specified client attached to it. Otherwise download it from remoteCache
     public UserSessionModel getUserSessionWithClient(RealmModel realm, String id, boolean offline, String clientUUID) {
-        return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessions().containsKey(clientUUID));
+        return kcSession.sessions().getUserSessionWithPredicate(realm, id, offline, userSession -> userSession.getAuthenticatedClientSessionByClient(clientUUID) != null);
     }
 
 
@@ -52,8 +52,8 @@ public class UserSessionCrossDCManager {
 
         return kcSession.sessions().getUserSessionWithPredicate(realm, id, false, (UserSessionModel userSession) -> {
 
-            Map<String, AuthenticatedClientSessionModel> authSessions = userSession.getAuthenticatedClientSessions();
-            return authSessions.containsKey(clientUUID);
+            AuthenticatedClientSessionModel authSessions = userSession.getAuthenticatedClientSessionByClient(clientUUID);
+            return authSessions != null;
 
         });
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
index f347d4c..1bef11e 100644
--- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
@@ -63,7 +63,7 @@ public class UserSessionManager {
         }
 
         // Create and persist clientSession
-        AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().get(clientSession.getClient().getId());
+        AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessionByClient(clientSession.getClient().getId());
         if (offlineClientSession == null) {
             createOfflineClientSession(user, clientSession, offlineUserSession);
         }
@@ -97,14 +97,14 @@ public class UserSessionManager {
         List<UserSessionModel> userSessions = kcSession.sessions().getOfflineUserSessions(realm, user);
         boolean anyRemoved = false;
         for (UserSessionModel userSession : userSessions) {
-            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
             if (clientSession != null) {
                 if (logger.isTraceEnabled()) {
                     logger.tracef("Removing existing offline token for user '%s' and client '%s' .",
                             user.getUsername(), client.getClientId());
                 }
 
-                clientSession.setUserSession(null);
+                clientSession.detachFromUserSession();
                 persister.removeClientSession(userSession.getId(), client.getId(), true);
                 checkOfflineUserSessionHasClientSessions(realm, user, userSession);
                 anyRemoved = true;
@@ -154,7 +154,8 @@ public class UserSessionManager {
 
     // Check if userSession has any offline clientSessions attached to it. Remove userSession if not
     private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession) {
-        if (userSession.getAuthenticatedClientSessions().size() > 0) {
+        // TODO: Might need optimization to prevent loading client sessions from cache
+        if (! userSession.getAuthenticatedClientSessions().isEmpty()) {
             return;
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
index 1f73bc6..3cfb6d3 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java
@@ -142,7 +142,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
         if (authResult != null) {
             UserSessionModel userSession = authResult.getSession();
             if (userSession != null) {
-                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
                 if (clientSession == null) {
                     clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
                 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index 34bbcea..dcfd651 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -339,7 +339,7 @@ public class UserResource {
             UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
 
             // Update lastSessionRefresh with the timestamp from clientSession
-            AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessions().get(clientId);
+            AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessionByClient(clientId);
 
             // Skip if userSession is not for this client
             if (clientSession == null) {
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli
index 3eee2fc..fd08666 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/crossdc/cross-dc-setup.cli
@@ -60,6 +60,38 @@ echo ** Update distributed-cache offlineSessions element **
 )
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)
 
+echo ** Update distributed-cache clientSessions element **
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( \
+    passivation=false, \
+    fetch-state=false, \
+    purge=false, \
+    preload=false, \
+    shared=true, \
+    remote-servers=["remote-cache"], \
+    cache=clientSessions, \
+    properties={ \
+        rawValues=true, \
+        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
+    } \
+)
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)
+
+echo ** Update distributed-cache offlineClientSessions element **
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( \
+    passivation=false, \
+    fetch-state=false, \
+    purge=false, \
+    preload=false, \
+    shared=true, \
+    remote-servers=["remote-cache"], \
+    cache=offlineClientSessions, \
+    properties={ \
+        rawValues=true, \
+        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory \
+    } \
+)
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true)
+
 echo ** Update distributed-cache loginFailures element **
 /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \
     passivation=false, \
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index d5395f2..6977310 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -185,6 +185,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
             throw new NotFoundException("Session not found");
         }
 
+        // TODO: Might need optimization to prevent loading client sessions from cache
         return sessionModel.getAuthenticatedClientSessions().size();
     }
 
diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
index efc0400..8bd5149 100644
--- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
+++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl
@@ -42,9 +42,10 @@
                 </backups>
             </replicated-cache-configuration>
 
-
             <replicated-cache name="sessions" configuration="sessions-cfg" />
             <replicated-cache name="offlineSessions" configuration="sessions-cfg" />
+            <replicated-cache name="clientSessions" configuration="sessions-cfg" />
+            <replicated-cache name="offlineClientSessions" configuration="sessions-cfg" />
             <replicated-cache name="loginFailures" configuration="sessions-cfg" />
             <replicated-cache name="actionTokens" configuration="sessions-cfg" />
             <replicated-cache name="work" configuration="sessions-cfg" />
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
index 0986c17..9b68878 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/ConcurrentLoginClusterTest.java
@@ -76,7 +76,7 @@ public class ConcurrentLoginClusterTest extends ConcurrentLoginTest {
     @Override
     public void concurrentLoginSingleUser() throws Throwable {
         super.concurrentLoginSingleUser();
-        JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getJgroupsStats();
+        JGroupsStats stats = testingClient.testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getJgroupsStats();
         log.info("JGroups statistics: " + stats.statsAsString());
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
index 76f6a7e..2c1ca21 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java
@@ -388,10 +388,8 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
     private void setTimeOffsetOnAllStartedContainers(int offset) {
         backendTestingClients.entrySet().stream()
                 .filter(testingClientEntry -> testingClientEntry.getKey().isStarted())
-                .forEach(testingClientEntry -> {
-                    KeycloakTestingClient testingClient = testingClientEntry.getValue();
-                    testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset)));
-                });
+                .map(testingClientEntry -> testingClientEntry.getValue())
+                .forEach(testingClient -> testingClient.testing().setTimeOffset(Collections.singletonMap("offset", String.valueOf(offset))));
     }
 
     /**
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
index bf42536..8f04936 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java
@@ -26,7 +26,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.Assert;
 import org.keycloak.testsuite.Retry;
 import org.keycloak.testsuite.arquillian.ContainerInfo;
-import org.keycloak.testsuite.client.KeycloakTestingClient;
 import org.keycloak.testsuite.rest.representation.RemoteCacheStats;
 import org.keycloak.testsuite.util.OAuthClient;
 
@@ -57,7 +56,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         // Get statistics
         int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
         int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
-        int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+        int lsrr0 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
         log.infof("lsr00: %d, lsr10: %d, lsrr0: %d", lsr00, lsr10, lsrr0);
 
         Assert.assertEquals(lsr00, lsr10);
@@ -76,7 +75,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         Retry.execute(() -> {
             int lsr01 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
             int lsr11 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
-            int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+            int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
             log.infof("lsr01: %d, lsr11: %d, lsrr1: %d", lsr01, lsr11, lsrr1);
             
             Assert.assertEquals(lsr01, lsr11);
@@ -88,7 +87,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         disableDcOnLoadBalancer(DC.FIRST);
         enableDcOnLoadBalancer(DC.SECOND);
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
-        Assert.assertNull(tokenResponse.getAccessToken());
+        Assert.assertNull("Expecting no access token present", tokenResponse.getAccessToken());
         Assert.assertNotNull(tokenResponse.getError());
 
         // try refresh with new token on DC1. It should pass.
@@ -164,7 +163,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         Assert.assertEquals(lsr10, lsr11.get());
 
         // assert that lastSessionRefresh still the same on remoteCache
-        int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
+        int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
         Assert.assertEquals(lsr00, lsrr1);
         log.infof("lsrr1: %d", lsrr1);
 
@@ -187,7 +186,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
             Assert.assertTrue(lsr02.get() > lsr01.get());
             Assert.assertTrue(lsr12.get() > lsr11.get());
 
-            lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
+            lsrr2.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
             log.infof("lsrr2: %d", lsrr2.get());
             Assert.assertEquals(lsrr1, lsrr2.get());
 
@@ -218,7 +217,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
             Assert.assertTrue(lsr03.get() > lsr02.get());
             Assert.assertTrue(lsr13.get() > lsr12.get());
 
-            lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
+            lsrr3.set(getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId));
             log.infof("lsrr3: %d", lsrr3.get());
             Assert.assertTrue(lsrr3.get() > lsrr2.get());
 
@@ -232,7 +231,7 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
 
     private RemoteCacheStats getRemoteCacheStats(int dcIndex) {
         return getTestingClientForStartedNodeInDc(dcIndex).testing("test")
-                .cache(InfinispanConnectionProvider.SESSION_CACHE_NAME)
+                .cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME)
                 .getRemoteCacheStats();
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
index f4a7159..bc1243b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
@@ -112,7 +112,7 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void sessionsPreloadTest() throws Exception {
-        int sessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size();
+        int sessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size();
         log.infof("sessionsBefore: %d", sessionsBefore);
 
         // Create initial sessions
@@ -124,8 +124,8 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
         enableLoadBalancerNode(DC.SECOND, 0);
 
         // Ensure sessions are loaded in both 1st DC and 2nd DC
-        int sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size();
-        int sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.SESSION_CACHE_NAME).size();
+        int sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size();
+        int sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).size();
         log.infof("sessions01: %d, sessions02: %d", sessions01, sessions02);
         Assert.assertEquals(sessions01, sessionsBefore + SESSIONS_COUNT);
         Assert.assertEquals(sessions02, sessionsBefore + SESSIONS_COUNT);
@@ -144,13 +144,13 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void offlineSessionsPreloadTest() throws Exception {
-        int offlineSessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size();
+        int offlineSessionsBefore = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size();
         log.infof("offlineSessionsBefore: %d", offlineSessionsBefore);
 
         // Create initial sessions
         List<OAuthClient.AccessTokenResponse> tokenResponses = createInitialSessions(true);
 
-        int offlineSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size();
+        int offlineSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size();
         Assert.assertEquals(offlineSessions01, offlineSessionsBefore + SESSIONS_COUNT);
         log.infof("offlineSessions01: %d", offlineSessions01);
 
@@ -168,8 +168,8 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
         enableLoadBalancerNode(DC.SECOND, 0);
 
         // Ensure sessions are loaded in both 1st DC and 2nd DC
-        int offlineSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size();
-        int offlineSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).size();
+        int offlineSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size();
+        int offlineSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME).size();
         log.infof("offlineSessions11: %d, offlineSessions12: %d", offlineSessions11, offlineSessions12);
         Assert.assertEquals(offlineSessions11, offlineSessionsBefore + SESSIONS_COUNT);
         Assert.assertEquals(offlineSessions12, offlineSessionsBefore + SESSIONS_COUNT);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
index 2644a9a..1523b33 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java
@@ -102,10 +102,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testRealmRemoveSessions(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -116,7 +118,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).remove();
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
@@ -194,11 +196,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testRealmRemoveOfflineSessions(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -206,18 +208,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).remove();
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 200l); // Might be bigger messages as online sessions removed too.
     }
 
 
     @Test
     public void testLogoutAllInRealm(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -225,18 +227,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).logoutAll();
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
 
     @Test
     public void testPeriodicExpiration(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
+        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
 
         // Assert I am able to refresh
         OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
@@ -249,7 +251,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
 
         // Nothing yet expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened
-        assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, 5l);
 
 
@@ -268,7 +270,7 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
@@ -277,10 +279,10 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testUserRemoveSessions(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -292,17 +294,17 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
 
     @Test
     public void testUserRemoveOfflineSessions(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -314,18 +316,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
 
     @Test
     public void testLogoutUser(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -335,29 +337,29 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId());
 
         // Just one session expired. Limit 5 for sent_messages is just if "lastSessionRefresh" periodic thread happened
-        assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01 + SESSIONS_COUNT - 1, sessions02 + SESSIONS_COUNT - 1, remoteSessions01 + SESSIONS_COUNT - 1, remoteSessions02 + SESSIONS_COUNT - 1, 5l);
 
         // Logout all sessions for user now
         user.logout();
 
         // Assert sessions removed on node1 and node2 and on remote caches. Assert that count of messages sent between DCs is not too big.
-        assertStatisticsExpected("After user logout", InfinispanConnectionProvider.SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+        assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
                 sessions01, sessions02, remoteSessions01, remoteSessions02, 100l);
     }
 
 
     @Test
     public void testLogoutUserWithFailover(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
         // Start node2 on first DC
         startBackendNode(DC.FIRST, 1);
 
         // Don't include remote stats. Size is smaller because of distributed cache
-        List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false);
+        List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false);
 
         // Kill node2 now. Around 10 sessions (half of SESSIONS_COUNT) will be lost on Keycloak side. But not on infinispan side
         stopBackendNode(DC.FIRST, 1);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index 5bbf71e..78ec0e7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -562,7 +562,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
             RealmModel realmModel = session.getContext().getRealm();
             String clientUuid = realmModel.getClientByClientId(clientId).getId();
             UserSessionModel userSession = session.sessions().getUserSession(realmModel, sessionId);
-            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUuid);
+            AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUuid);
             
             String claimsInSession = clientSession.getNote(OIDCLoginProtocol.CLAIMS_PARAM);
             assertEquals(claimsJson, claimsInSession);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java
index 8946127..569a076 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/LastSessionRefreshUnitTest.java
@@ -169,7 +169,7 @@ public class LastSessionRefreshUnitTest extends AbstractKeycloakTest {
 
             };
 
-            Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+            Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
             return factory.createAndInit(session, cache, timerIntervalMs, maxIntervalBetweenMessagesSeconds, 10, false);
         }
 
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
index 8c01e2c..9162726 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
@@ -132,7 +132,6 @@ public class UserSessionInitializerTest {
 
     private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
         AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
-        if (userSession != null) clientSession.setUserSession(userSession);
         clientSession.setRedirectUri(redirect);
         if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
         if (roles != null) clientSession.setRoles(roles);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
index 8c89046..efa2f7d 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
@@ -362,7 +362,6 @@ public class UserSessionPersisterProviderTest {
 
     private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
         AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
-        if (userSession != null) clientSession.setUserSession(userSession);
         clientSession.setRedirectUri(redirect);
         if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
         if (roles != null) clientSession.setRoles(roles);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
index 106f525..265ecd7 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
@@ -396,7 +396,6 @@ public class UserSessionProviderOfflineTest {
 
     private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
         AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client, userSession);
-        if (userSession != null) clientSession.setUserSession(userSession);
         clientSession.setRedirectUri(redirect);
         if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
         if (roles != null) clientSession.setRoles(roles);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index e616331..3e2a4dc 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -33,7 +33,6 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.models.UserManager;
-import org.keycloak.sessions.CommonClientSessionModel;
 import org.keycloak.testsuite.rule.KeycloakRule;
 
 import java.util.Arrays;
@@ -182,6 +181,30 @@ public class UserSessionProviderTest {
     }
 
     @Test
+    public void testUpdateClientSessionWithGetByClientId() {
+        UserSessionModel[] sessions = createSessions();
+
+        String userSessionId = sessions[0].getId();
+        String clientUUID = realm.getClientByClientId("test-app").getId();
+
+        UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);
+
+        int time = clientSession.getTimestamp();
+        assertEquals(null, clientSession.getAction());
+
+        clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
+        clientSession.setTimestamp(time + 10);
+
+        kc.stopSession(session, true);
+        session = kc.startSession();
+
+        AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessionByClient(clientUUID);
+        assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction());
+        assertEquals(time + 10, updated.getTimestamp());
+    }
+
+    @Test
     public void testUpdateClientSessionInSameTransaction() {
         UserSessionModel[] sessions = createSessions();
 
@@ -189,12 +212,12 @@ public class UserSessionProviderTest {
         String clientUUID = realm.getClientByClientId("test-app").getId();
 
         UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
-        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
+        AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);
 
         clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
         clientSession.setNote("foo", "bar");
 
-        AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID);
+        AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessionByClient(clientUUID);
         assertEquals(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name(), updated.getAction());
         assertEquals("bar", updated.getNote("foo"));
     }
@@ -361,7 +384,6 @@ public class UserSessionProviderTest {
                 Time.setOffset(i);
                 UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null);
                 AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession);
-                clientSession.setUserSession(userSession);
                 clientSession.setRedirectUri("http://redirect");
                 clientSession.setRoles(new HashSet<String>());
                 clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state");
@@ -451,7 +473,7 @@ public class UserSessionProviderTest {
 
         // remove session
         clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId());
-        clientSession1.setUserSession(null);
+        clientSession1.detachFromUserSession();
 
         // Commit and ensure removed
         resetSession();
diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java
index 41495d4..4790e9c 100644
--- a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java
+++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java
@@ -31,19 +31,28 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public abstract class AbstractSessionCacheCommand extends AbstractCommand {
 
+    private static final Set<String> SUPPORTED_CACHE_NAMES = new TreeSet<>(Arrays.asList(
+      InfinispanConnectionProvider.USER_SESSION_CACHE_NAME,
+      InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME,
+      InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
+      InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME
+    ));
+
     @Override
     protected void doRunCommand(KeycloakSession session) {
         InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
         String cacheName = getArg(0);
-        if (!cacheName.equals(InfinispanConnectionProvider.SESSION_CACHE_NAME) && !cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME)) {
-            log.errorf("Invalid cache name: '%s', Only cache names '%s' or '%s' are supported", cacheName, InfinispanConnectionProvider.SESSION_CACHE_NAME,
-                    InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+        if (! SUPPORTED_CACHE_NAMES.contains(cacheName)) {
+            log.errorf("Invalid cache name: '%s', Only cache names '%s' are supported", cacheName, SUPPORTED_CACHE_NAMES);
             throw new HandledException();
         }
 
diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
index 53e97a5..563df20 100755
--- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
+++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
@@ -41,7 +41,7 @@ import java.util.List;
 public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor {
 
     private static final String[] CACHES = new String[] {
-        "realms", "users","sessions","authenticationSessions","offlineSessions","loginFailures","work","authorization","keys","actionTokens"
+        "realms", "users","sessions","authenticationSessions","offlineSessions","clientSessions","offlineClientSessions","loginFailures","work","authorization","keys","actionTokens"
     };
 
     // This param name is defined again in Keycloak Services class
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 8702948..b3ea2a9 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -34,6 +34,8 @@
                 <local-cache name="sessions"/>
                 <local-cache name="authenticationSessions"/>
                 <local-cache name="offlineSessions"/>
+                <local-cache name="clientSessions"/>
+                <local-cache name="offlineClientSessions"/>
                 <local-cache name="loginFailures"/>
                 <local-cache name="work"/>
                 <local-cache name="authorization">
@@ -94,6 +96,8 @@
                 <distributed-cache name="sessions" mode="SYNC" owners="1"/>
                 <distributed-cache name="authenticationSessions" mode="SYNC" owners="1"/>
                 <distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
+                <distributed-cache name="clientSessions" mode="SYNC" owners="1"/>
+                <distributed-cache name="offlineClientSessions" mode="SYNC" owners="1"/>
                 <distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
                 <local-cache name="authorization">
                     <eviction max-entries="10000" strategy="LRU"/>