keycloak-aplcache

KEYCLOAK-5747 Ensure refreshToken doesn't need to send request

10/27/2017 12:14:08 PM

Changes

Details

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 572c5f0..7493cae 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
@@ -304,12 +304,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
         cacheManager.defineConfiguration(InfinispanConnectionProvider.WORK_CACHE_NAME, replicationEvictionCacheConfiguration);
 
-        ConfigurationBuilder counterConfigBuilder = new ConfigurationBuilder();
-        counterConfigBuilder.invocationBatching().enable()
-                .transaction().transactionMode(TransactionMode.TRANSACTIONAL);
-        counterConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
-        counterConfigBuilder.transaction().lockingMode(LockingMode.PESSIMISTIC);
-
         long realmRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.REALM_CACHE_NAME).getCacheConfiguration().eviction().maxEntries();
         realmRevisionsMaxEntries = realmRevisionsMaxEntries > 0
                 ? 2 * realmRevisionsMaxEntries
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 736e756..cf8f577 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
@@ -24,16 +24,16 @@ import java.util.Set;
 
 import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
 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.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.changes.sessions.LastSessionRefreshChecker;
 import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 import java.util.UUID;
@@ -43,25 +43,31 @@ import java.util.UUID;
  */
 public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
 
+    private final KeycloakSession kcSession;
+    private final InfinispanUserSessionProvider provider;
     private AuthenticatedClientSessionEntity entity;
     private final ClientModel client;
     private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
     private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
     private UserSessionModel userSession;
+    private boolean offline;
 
-    public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client,
-                                             UserSessionModel userSession,
+    public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
+                                             AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
                                              InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
-                                             InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
+                                             InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
         if (userSession == null) {
             throw new NullPointerException("userSession must not be null");
         }
 
+        this.kcSession = kcSession;
+        this.provider = provider;
         this.entity = entity;
         this.userSession = userSession;
         this.client = client;
         this.userSessionUpdateTx = userSessionUpdateTx;
         this.clientSessionUpdateTx = clientSessionUpdateTx;
+        this.offline = offline;
     }
 
     private void update(UserSessionUpdateTask task) {
@@ -141,6 +147,18 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
             public void runUpdate(AuthenticatedClientSessionEntity entity) {
                 entity.setTimestamp(timestamp);
             }
+
+            @Override
+            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<AuthenticatedClientSessionEntity> sessionWrapper) {
+                return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
+                        .shouldSaveClientSessionToRemoteCache(kcSession, client.getRealm(), sessionWrapper, userSession, offline, timestamp);
+            }
+
+            @Override
+            public String toString() {
+                return "setTimestamp(" + timestamp + ')';
+            }
+
         };
 
         update(task);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
index 25e0df9..6d2e8e2 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
@@ -17,11 +17,16 @@
 
 package org.keycloak.models.sessions.infinispan.changes.sessions;
 
+import java.util.UUID;
+
 import org.jboss.logging.Logger;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
 /**
@@ -41,7 +46,72 @@ public class LastSessionRefreshChecker {
     }
 
 
-    public SessionUpdateTask.CrossDCMessageStatus getCrossDCMessageStatus(KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
+    public SessionUpdateTask.CrossDCMessageStatus shouldSaveUserSessionToRemoteCache(
+            KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
+
+        SessionUpdateTask.CrossDCMessageStatus baseChecks = baseChecks(kcSession, realm ,offline);
+        if (baseChecks != null) {
+            return baseChecks;
+        }
+
+        String userSessionId = sessionWrapper.getEntity().getId();
+
+        if (offline) {
+            Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
+            if (lsrr == null) {
+                lsrr = sessionWrapper.getEntity().getStarted();
+            }
+
+            if (lsrr + (realm.getOfflineSessionIdleTimeout() / 2) <= newLastSessionRefresh) {
+                logger.debugf("We are going to write remotely userSession %s. Remote last session refresh: %d, New last session refresh: %d",
+                        userSessionId, lsrr, newLastSessionRefresh);
+                return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+            }
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", userSessionId, newLastSessionRefresh);
+        }
+
+        LastSessionRefreshStore storeToUse = offline ? offlineStore : store;
+        storeToUse.putLastSessionRefresh(kcSession, userSessionId, realm.getId(), newLastSessionRefresh);
+
+        return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
+    }
+
+
+    public SessionUpdateTask.CrossDCMessageStatus shouldSaveClientSessionToRemoteCache(
+            KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<AuthenticatedClientSessionEntity> sessionWrapper, UserSessionModel userSession, boolean offline, int newTimestamp) {
+
+        SessionUpdateTask.CrossDCMessageStatus baseChecks = baseChecks(kcSession, realm ,offline);
+        if (baseChecks != null) {
+            return baseChecks;
+        }
+
+        UUID clientSessionId = sessionWrapper.getEntity().getId();
+
+        if (offline) {
+            Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(AuthenticatedClientSessionEntity.LAST_TIMESTAMP_REMOTE);
+            if (lsrr == null) {
+                lsrr = userSession.getStarted();
+            }
+
+            if (lsrr + (realm.getOfflineSessionIdleTimeout() / 2) <= newTimestamp) {
+                    logger.debugf("We are going to write remotely for clientSession %s. Remote timestamp: %d, New timestamp: %d",
+                            clientSessionId, lsrr, newTimestamp);
+                return SessionUpdateTask.CrossDCMessageStatus.SYNC;
+            }
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debugf("Skip writing timestamp to the remoteCache. ClientSession %s timestamp %d", clientSessionId, newTimestamp);
+        }
+
+        return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
+    }
+
+
+    private SessionUpdateTask.CrossDCMessageStatus baseChecks(KeycloakSession kcSession, RealmModel realm, boolean offline) {
         // revokeRefreshToken always writes everything to remoteCache immediately
         if (realm.isRevokeRefreshToken()) {
             return SessionUpdateTask.CrossDCMessageStatus.SYNC;
@@ -53,29 +123,13 @@ public class LastSessionRefreshChecker {
             return SessionUpdateTask.CrossDCMessageStatus.SYNC;
         }
 
+        // Received the message from the other DC that we should update the lastSessionRefresh in local cluster
         Boolean ignoreRemoteCacheUpdate = (Boolean) kcSession.getAttribute(LastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE);
         if (ignoreRemoteCacheUpdate != null && ignoreRemoteCacheUpdate) {
             return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
         }
 
-        Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
-        if (lsrr == null) {
-            logger.debugf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
-            return SessionUpdateTask.CrossDCMessageStatus.SYNC;
-        }
-
-        int idleTimeout = offline ? realm.getOfflineSessionIdleTimeout() : realm.getSsoSessionIdleTimeout();
-
-        if (lsrr + (idleTimeout / 2) <= newLastSessionRefresh) {
-            logger.debugf("We are going to write remotely. Remote last session refresh: %d, New last session refresh: %d", (int) lsrr, newLastSessionRefresh);
-            return SessionUpdateTask.CrossDCMessageStatus.SYNC;
-        }
-
-        logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", sessionWrapper.getEntity().getId(), newLastSessionRefresh);
-
-        storeToUse.putLastSessionRefresh(kcSession, sessionWrapper.getEntity().getId(), realm.getId(), newLastSessionRefresh);
-
-        return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
+        return null;
     }
 
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
index 892ecfe..00b499e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
@@ -79,9 +79,9 @@ public class LastSessionRefreshListener implements ClusterListener {
                 KeycloakModelUtils.runJobInTransaction(sessionFactory, (kcSession) -> {
 
                     RealmModel realm = kcSession.realms().getRealm(realmId);
-                    UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId);
+                    UserSessionModel userSession = offline ? kcSession.sessions().getOfflineUserSession(realm, sessionId) : kcSession.sessions().getUserSession(realm, sessionId);
                     if (userSession == null) {
-                        logger.debugf("User session %s not available on node %s", sessionId, myAddress);
+                        logger.debugf("User session '%s' not available on node '%s' offline '%b'", sessionId, myAddress, offline);
                     } else {
                         // Update just if lastSessionRefresh from event is bigger than ours
                         if (lastSessionRefresh > userSession.getLastSessionRefresh()) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java
index d7b8559..6db17d0 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java
@@ -23,6 +23,7 @@ import org.keycloak.common.util.Time;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 import org.keycloak.timer.TimerProvider;
 
 /**
@@ -30,15 +31,19 @@ import org.keycloak.timer.TimerProvider;
  */
 public class LastSessionRefreshStoreFactory {
 
-    // Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes
+    // Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes should be sent
     public static final long DEFAULT_TIMER_INTERVAL_MS = 5000;
 
     // Max interval between messages. It means that when message is sent to second DC, then another message will be sent at least after 60 seconds.
-    public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = 60;
+    public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = SessionTimeoutHelper.PERIODIC_TASK_INTERVAL_SECONDS;
 
-    // Max count of lastSessionRefreshes. It count of lastSessionRefreshes reach this value, the message is sent to second DC
+    // Max count of lastSessionRefreshes. If count of lastSessionRefreshes reach this value, the message is sent to second DC
     public static final int DEFAULT_MAX_COUNT = 100;
 
+    // Name of periodic tasks to send events to the other DCs
+    public static final String LSR_PERIODIC_TASK_NAME = "lastSessionRefreshes";
+    public static final String LSR_OFFLINE_PERIODIC_TASK_NAME = "lastSessionRefreshes-offline";
+
 
     public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, boolean offline) {
         return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline);
@@ -46,7 +51,7 @@ public class LastSessionRefreshStoreFactory {
 
 
     public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) {
-        String eventKey = offline ? "lastSessionRefreshes-offline" :  "lastSessionRefreshes";
+        String eventKey = offline ? LSR_OFFLINE_PERIODIC_TASK_NAME :  LSR_PERIODIC_TASK_NAME;
         LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
 
         // Register listener
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 18d892f..16ce4ab 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
@@ -27,6 +27,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import org.infinispan.commons.marshall.Externalizer;
 import org.infinispan.commons.marshall.MarshallUtil;
 import org.infinispan.commons.marshall.SerializeWith;
+import org.jboss.logging.Logger;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
 import java.util.UUID;
 
@@ -37,6 +39,11 @@ import java.util.UUID;
 @SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class)
 public class AuthenticatedClientSessionEntity extends SessionEntity {
 
+    public static final Logger logger = Logger.getLogger(AuthenticatedClientSessionEntity.class);
+
+    // Metadata attribute, which contains the last timestamp available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
+    public static final String LAST_TIMESTAMP_REMOTE = "lstr";
+
     private String authMethod;
     private String redirectUri;
     private volatile int timestamp;
@@ -157,6 +164,31 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
         return id != null ? id.hashCode() : 0;
     }
 
+    @Override
+    public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
+        int timestampRemote = getTimestamp();
+
+        SessionEntityWrapper entityWrapper;
+        if (localEntityWrapper == null) {
+            entityWrapper = new SessionEntityWrapper<>(this);
+        } else {
+            AuthenticatedClientSessionEntity localClientSession = (AuthenticatedClientSessionEntity) localEntityWrapper.getEntity();
+
+            // local timestamp should always contain the bigger
+            if (timestampRemote < localClientSession.getTimestamp()) {
+                setTimestamp(localClientSession.getTimestamp());
+            }
+
+            entityWrapper = new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
+        }
+
+        entityWrapper.putLocalMetadataNoteInt(LAST_TIMESTAMP_REMOTE, timestampRemote);
+
+        logger.debugf("Updating client session entity %s. timestamp=%d, timestampRemote=%d", getId(), getTimestamp(), timestampRemote);
+
+        return entityWrapper;
+    }
+
     public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
 
         @Override
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 dbde092..fafd156 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
@@ -219,7 +219,7 @@ public class UserSessionEntity extends SessionEntity {
 
         entityWrapper.putLocalMetadataNoteInt(LAST_SESSION_REFRESH_REMOTE, lsrRemote);
 
-        logger.debugf("Updating session entity. lastSessionRefresh=%d, lastSessionRefreshRemote=%d", getLastSessionRefresh(), lsrRemote);
+        logger.debugf("Updating session entity '%s'. lastSessionRefresh=%d, lastSessionRefreshRemote=%d", getId(), getLastSessionRefresh(), lsrRemote);
 
         return entityWrapper;
     }
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 2c0411e..5015130 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
@@ -38,8 +38,6 @@ 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;
@@ -56,6 +54,7 @@ import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
 import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
 import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
 import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -164,8 +163,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
         InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
-        AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession,
-          userSessionUpdateTx, clientSessionUpdateTx);
+        AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, false);
 
         SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
         clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity);
@@ -446,17 +444,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     private void removeExpiredUserSessions(RealmModel realm) {
         int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
-        int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
+        int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
 
         FuturesHelper futures = new FuturesHelper();
 
         // 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<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(clientSessionCache);
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
         final AtomicInteger userSessionsSize = new AtomicInteger();
+        final AtomicInteger clientSessionsSize = new AtomicInteger();
 
         // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
         localCacheStoreIgnore
@@ -474,6 +473,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                         futures.addTask(future);
 
                         userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
+                            clientSessionsSize.incrementAndGet();
                             Future f = localClientSessionCache.removeAsync(clientSessionId);
                             futures.addTask(f);
                         });
@@ -483,12 +483,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         futures.waitForAllToFinish();
 
-        log.debugf("Removed %d expired user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
+        log.debugf("Removed %d expired user sessions and %d expired client sessions for realm '%s'", userSessionsSize.get(),
+                clientSessionsSize.get(), realm.getName());
     }
 
     private void removeExpiredOfflineUserSessions(RealmModel realm) {
         UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
-        int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
+        int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
 
         // 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);
@@ -501,6 +502,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
         final AtomicInteger userSessionsSize = new AtomicInteger();
+        final AtomicInteger clientSessionsSize = new AtomicInteger();
 
         // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
         localCacheStoreIgnore
@@ -517,6 +519,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                         Future future = localCache.removeAsync(userSessionEntity.getId());
                         futures.addTask(future);
                         userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
+                            clientSessionsSize.incrementAndGet();
                             Future f = localClientSessionCache.removeAsync(clientSessionId);
                             futures.addTask(f);
                         });
@@ -533,7 +536,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         futures.waitForAllToFinish();
 
-        log.debugf("Removed %d expired offline user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
+        log.debugf("Removed %d expired offline user sessions and %d expired offline client sessions for realm '%s'",
+                userSessionsSize.get(), clientSessionsSize.get(), realm.getName());
     }
 
     @Override
@@ -712,7 +716,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     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;
+        return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, offline) : null;
     }
 
     UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@@ -762,7 +766,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
         InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(true);
         InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(true);
-        AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
+        AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true);
 
         // update timestamp to current time
         offlineClientSession.setTimestamp(Time.currentTime());
@@ -831,7 +835,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         // Handle client sessions
         if (importAuthenticatedClientSessions) {
             for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
-                importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
+                importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx, offline);
             }
         }
 
@@ -841,7 +845,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession,
                                                                   InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
-                                                                  InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
+                                                                  InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
+                                                                  boolean offline) {
         AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
         entity.setRealmId(sessionToImportInto.getRealm().getId());
         final UUID clientSessionId = entity.getId();
@@ -864,7 +869,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
         userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
 
-        return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx);
+        return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx, offline);
     }
 
     private static class RegisterClientSessionTask implements SessionUpdateTask<UserSessionEntity> {
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 22b7382..697c3f2 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
@@ -212,7 +212,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
         boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
-            return realm.getSsoSessionIdleTimeout() * 1000;
+            // We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
+            return realm.getSsoSessionMaxLifespan() * 1000;
         });
 
         if (sessionsRemoteCache) {
@@ -221,7 +222,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
 
         Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
         checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
-            return realm.getSsoSessionIdleTimeout() * 1000;
+            // We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
+            return realm.getSsoSessionMaxLifespan() * 1000;
         });
 
         Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
index 404d3ca..e32b919 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
@@ -19,7 +19,6 @@ package org.keycloak.models.sessions.infinispan.remotestore;
 
 import org.infinispan.client.hotrod.exceptions.HotRodClientException;
 import org.keycloak.common.util.Retry;
-import org.keycloak.common.util.Time;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -34,9 +33,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
-import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
-import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -78,8 +75,8 @@ public class RemoteCacheInvoker {
 
         long loadedMaxIdleTimeMs = context.maxIdleTimeLoader.getMaxIdleTimeMs(realm);
 
-        // Double the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
-        final long maxIdleTimeMs = loadedMaxIdleTimeMs * 2;
+        // Increase the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
+        final long maxIdleTimeMs = loadedMaxIdleTimeMs + 1800000;
 
         if (logger.isTraceEnabled()) {
             logger.tracef("Running task '%s' on remote cache '%s' . Key is '%s'", operation, cacheName, key);
@@ -115,7 +112,6 @@ public class RemoteCacheInvoker {
                 remoteCache.put(key, sessionWrapper.forTransport(), task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
                 break;
             case ADD_IF_ABSENT:
-                final int currentTime = Time.currentTime();
                 SessionEntityWrapper<V> existing = remoteCache
                         .withFlags(Flag.FORCE_RETURN_VALUE)
                         .putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
@@ -123,8 +119,6 @@ public class RemoteCacheInvoker {
                     logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
 
                     replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
-                } else {
-                    sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
                 }
                 break;
             case REPLACE:
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 de82557..d0c2a4a 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
@@ -212,7 +212,7 @@ public class UserSessionAdapter implements UserSessionModel {
             @Override
             public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
                 return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
-                        .getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
+                        .shouldSaveUserSessionToRemoteCache(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
             }
 
             @Override
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 0d28458..2e4428a 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
@@ -76,6 +76,7 @@ public class ConcurrencyJDGSessionsCacheTest {
 
     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.USER_SESSION_CACHE_NAME);
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
@@ -187,7 +188,6 @@ public class ConcurrencyJDGSessionsCacheTest {
                 ", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
                 ", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
 
-
         System.out.println("remoteCache1.notes: " + ((UserSessionEntity) remoteCache1.get("123")).getNotes().size() );
         System.out.println("remoteCache2.notes: " + ((UserSessionEntity) remoteCache2.get("123")).getNotes().size() );
 
diff --git a/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java b/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
index 030e5a0..a9da1d7 100644
--- a/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
+++ b/model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java
@@ -156,12 +156,13 @@ public class InfinispanKeyStorageProviderTest {
 
     protected Cache<String, PublicKeysEntry> getKeysCache() {
         GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
-        gcb.globalJmxStatistics().allowDuplicateDomains(true);
+        gcb.globalJmxStatistics().allowDuplicateDomains(true).enabled(true);
 
         final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
 
         ConfigurationBuilder cb = new ConfigurationBuilder();
         cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
+        cb.jmxStatistics().enabled(true);
         Configuration cfg = cb.build();
 
         cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, cfg);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/SessionTimeoutHelper.java b/server-spi-private/src/main/java/org/keycloak/models/utils/SessionTimeoutHelper.java
new file mode 100644
index 0000000..b52d185
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/SessionTimeoutHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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.utils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SessionTimeoutHelper {
+
+
+    /**
+     * Interval specifies maximum time, for which the "userSession.lastSessionRefresh" may contain stale value.
+     *
+     * For example, if there are 2 datacenters and sessionRefresh will happen on DC1, then the message about the updated lastSessionRefresh may
+     * be sent to the DC2 later (EG. Some periodic thread will send the updated lastSessionRefresh times in batches with 60 seconds delay).
+     */
+    public static final int PERIODIC_TASK_INTERVAL_SECONDS = 60;
+
+
+    /**
+     * The maximum time difference, which will be still tolerated when checking userSession idle timeout.
+     *
+     * For example, if there are 2 datacenters and sessionRefresh happened on DC1, then we still want to tolerate some timeout on DC2 due the
+     * fact that lastSessionRefresh of current userSession may be updated later from DC1.
+     *
+     * See {@link #PERIODIC_TASK_INTERVAL_SECONDS}
+     */
+    public static final int IDLE_TIMEOUT_WINDOW_SECONDS = 120;
+
+
+    /**
+     * The maximum time difference, which will be still tolerated when checking userSession idle timeout with periodic cleaner threads.
+     *
+     * Just the sessions, with the timeout bigger than this value are considered really time-outed and can be garbage-collected (Considering the cross-dc
+     * environment and the fact that some session updates on different DC can be postponed and seen on current DC with some delay).
+     *
+     * See {@link #PERIODIC_TASK_INTERVAL_SECONDS} and {@link #IDLE_TIMEOUT_WINDOW_SECONDS}
+     */
+    public static final int PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS = 180;
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/timer/TimerProvider.java b/server-spi-private/src/main/java/org/keycloak/timer/TimerProvider.java
index 5dbf69b..7b27941 100644
--- a/server-spi-private/src/main/java/org/keycloak/timer/TimerProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/timer/TimerProvider.java
@@ -28,6 +28,21 @@ public interface TimerProvider extends Provider {
 
     public void scheduleTask(ScheduledTask scheduledTask, long intervalMillis, String taskName);
 
-    public void cancelTask(String taskName);
+
+    /**
+     * Cancel task and return the details about it, so it can be eventually restored later
+     *
+     * @param taskName
+     * @return existing task or null if task under this name doesn't exist
+     */
+    public TimerTaskContext cancelTask(String taskName);
+
+
+    interface TimerTaskContext {
+
+        Runnable getRunnable();
+
+        long getIntervalMillis();
+    }
 
 }
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 d0774c4..47f44e5 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -132,7 +132,7 @@ public class TokenManager {
             if (userSession != null) {
 
                 // Revoke timeouted offline userSession
-                if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) {
+                if (!AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
                     sessionManager.revokeOfflineUserSession(userSession);
                     throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
                 }
@@ -282,9 +282,8 @@ public class TokenManager {
                     clusterStartupTime != validation.clientSession.getTimestamp()) {
                 throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
             }
-        }
 
-        if (realm.isRevokeRefreshToken()) {
+
             if (!refreshToken.getId().equals(validation.clientSession.getCurrentRefreshToken())) {
                 validation.clientSession.setCurrentRefreshToken(refreshToken.getId());
                 validation.clientSession.setCurrentRefreshTokenUseCount(0);
@@ -296,8 +295,6 @@ public class TokenManager {
                         "Maximum allowed refresh token reuse exceeded");
             }
             validation.clientSession.setCurrentRefreshTokenUseCount(currentCount + 1);
-        } else {
-            validation.clientSession.setCurrentRefreshToken(null);
         }
     }
 
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 32f8645..8791010 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -41,6 +41,7 @@ import org.keycloak.jose.jws.AlgorithmType;
 import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.models.*;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 import org.keycloak.protocol.LoginProtocol;
 import org.keycloak.protocol.LoginProtocol.Error;
 import org.keycloak.protocol.oidc.TokenManager;
@@ -107,7 +108,11 @@ public class AuthenticationManager {
         }
         int currentTime = Time.currentTime();
         int max = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
-        return userSession.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
+
+        // Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
+        int maxIdle = realm.getSsoSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
+
+        return userSession.getLastSessionRefresh() + maxIdle > currentTime && max > currentTime;
     }
 
     public static boolean isOfflineSessionValid(RealmModel realm, UserSessionModel userSession) {
@@ -116,7 +121,11 @@ public class AuthenticationManager {
             return false;
         }
         int currentTime = Time.currentTime();
-        return userSession.getLastSessionRefresh() + realm.getOfflineSessionIdleTimeout() > currentTime;
+
+        // Additional time window is added for the case when session was updated in different DC and the update to current DC was postponed
+        int maxIdle = realm.getOfflineSessionIdleTimeout() + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS;
+
+        return userSession.getLastSessionRefresh() + maxIdle > currentTime;
     }
 
     public static void expireUserSessionCookie(KeycloakSession session, UserSessionModel userSession, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, ClientConnection connection) {
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index ae0979d..d4e2905 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -334,7 +334,7 @@ public class KeycloakApplication extends Application {
             TimerProvider timer = session.getProvider(TimerProvider.class);
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredClientInitialAccessTokens(), interval), interval, "ClearExpiredClientInitialAccessTokens");
-            timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
+            timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, ClearExpiredUserSessions.TASK_NAME);
             new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
         } finally {
             session.close();
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
index 5315be4..e61969a 100755
--- a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
@@ -27,6 +27,8 @@ import org.keycloak.timer.ScheduledTask;
  */
 public class ClearExpiredUserSessions implements ScheduledTask {
 
+    public static final String TASK_NAME = "ClearExpiredUserSessions";
+
     @Override
     public void run(KeycloakSession session) {
         UserSessionProvider sessions = session.sessions();
diff --git a/services/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java b/services/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java
index 29a736f..73f38be 100644
--- a/services/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java
+++ b/services/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java
@@ -52,10 +52,11 @@ public class BasicTimerProvider implements TimerProvider {
             }
         };
 
-        TimerTask existingTask = factory.putTask(taskName, task);
+        TimerTaskContextImpl taskContext = new TimerTaskContextImpl(runnable, task, intervalMillis);
+        TimerTaskContextImpl existingTask = factory.putTask(taskName, taskContext);
         if (existingTask != null) {
             logger.debugf("Existing timer task '%s' found. Cancelling it", taskName);
-            existingTask.cancel();
+            existingTask.timerTask.cancel();
         }
 
         logger.debugf("Starting task '%s' with interval '%d'", taskName, intervalMillis);
@@ -69,12 +70,14 @@ public class BasicTimerProvider implements TimerProvider {
     }
 
     @Override
-    public void cancelTask(String taskName) {
-        TimerTask existingTask = factory.removeTask(taskName);
+    public TimerTaskContext cancelTask(String taskName) {
+        TimerTaskContextImpl existingTask = factory.removeTask(taskName);
         if (existingTask != null) {
             logger.debugf("Cancelling task '%s'", taskName);
-            existingTask.cancel();
+            existingTask.timerTask.cancel();
         }
+
+        return existingTask;
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java b/services/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java
index ea0da94..06559bc 100755
--- a/services/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java
+++ b/services/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java
@@ -35,7 +35,7 @@ public class BasicTimerProviderFactory implements TimerProviderFactory {
 
     private Timer timer;
 
-    private ConcurrentMap<String, TimerTask> scheduledTasks = new ConcurrentHashMap<String, TimerTask>();
+    private ConcurrentMap<String, TimerTaskContextImpl> scheduledTasks = new ConcurrentHashMap<>();
 
     @Override
     public TimerProvider create(KeycloakSession session) {
@@ -63,11 +63,11 @@ public class BasicTimerProviderFactory implements TimerProviderFactory {
         return "basic";
     }
 
-    protected TimerTask putTask(String taskName, TimerTask task) {
+    protected TimerTaskContextImpl putTask(String taskName, TimerTaskContextImpl task) {
         return scheduledTasks.put(taskName, task);
     }
 
-    protected TimerTask removeTask(String taskName) {
+    protected TimerTaskContextImpl removeTask(String taskName) {
         return scheduledTasks.remove(taskName);
     }
 
diff --git a/services/src/main/java/org/keycloak/timer/basic/TimerTaskContextImpl.java b/services/src/main/java/org/keycloak/timer/basic/TimerTaskContextImpl.java
new file mode 100644
index 0000000..c53cb87
--- /dev/null
+++ b/services/src/main/java/org/keycloak/timer/basic/TimerTaskContextImpl.java
@@ -0,0 +1,48 @@
+/*
+ * 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.timer.basic;
+
+import java.util.TimerTask;
+
+import org.keycloak.timer.TimerProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TimerTaskContextImpl implements TimerProvider.TimerTaskContext {
+
+    private final Runnable runnable;
+    final TimerTask timerTask;
+    private final long intervalMillis;
+
+    public TimerTaskContextImpl(Runnable runnable, TimerTask timerTask, long intervalMillis) {
+        this.runnable = runnable;
+        this.timerTask = timerTask;
+        this.intervalMillis = intervalMillis;
+    }
+
+    @Override
+    public Runnable getRunnable() {
+        return runnable;
+    }
+
+    @Override
+    public long getIntervalMillis() {
+        return intervalMillis;
+    }
+}
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 6977310..953cb95 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
@@ -40,6 +40,7 @@ import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserProvider;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.AdminEventRepresentation;
@@ -49,6 +50,7 @@ import org.keycloak.representations.idm.EventRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.scheduled.ClearExpiredUserSessions;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.testsuite.components.TestProvider;
 import org.keycloak.testsuite.components.TestProviderFactory;
@@ -63,6 +65,7 @@ import org.keycloak.testsuite.runonserver.ModuleUtil;
 import org.keycloak.testsuite.runonserver.FetchOnServer;
 import org.keycloak.testsuite.runonserver.RunOnServer;
 import org.keycloak.testsuite.runonserver.SerializationUtil;
+import org.keycloak.timer.TimerProvider;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.utils.MediaType;
 
@@ -83,21 +86,24 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TimerTask;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TestingResourceProvider implements RealmResourceProvider {
 
-    private KeycloakSession session;
+    private final KeycloakSession session;
+    private final Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks;
 
     @Override
     public Object getResource() {
         return this;
     }
 
-    public TestingResourceProvider(KeycloakSession session) {
+    public TestingResourceProvider(KeycloakSession session, Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks) {
         this.session = session;
+        this.suspendedTimerTasks = suspendedTimerTasks;
     }
 
     @POST
@@ -134,9 +140,9 @@ public class TestingResourceProvider implements RealmResourceProvider {
     }
 
     @GET
-    @Path("/get-user-session")
+    @Path("/get-last-session-refresh")
     @Produces(MediaType.APPLICATION_JSON)
-    public Integer getLastSessionRefresh(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId) {
+    public Integer getLastSessionRefresh(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId, @QueryParam("offline") boolean offline) {
 
         RealmManager realmManager = new RealmManager(session);
         RealmModel realm = realmManager.getRealmByName(name);
@@ -144,7 +150,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
             throw new NotFoundException("Realm not found");
         }
 
-        UserSessionModel sessionModel = session.sessions().getUserSession(realm, sessionId);
+        UserSessionModel sessionModel = offline ? session.sessions().getOfflineUserSession(realm, sessionId) : session.sessions().getUserSession(realm, sessionId);
         if (sessionModel == null) {
             throw new NotFoundException("Session not found");
         }
@@ -674,6 +680,41 @@ public class TestingResourceProvider implements RealmResourceProvider {
     }
 
     @POST
+    @Path("/suspend-periodic-tasks")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response suspendPeriodicTasks() {
+        suspendTask(ClearExpiredUserSessions.TASK_NAME);
+        suspendTask(LastSessionRefreshStoreFactory.LSR_PERIODIC_TASK_NAME);
+        suspendTask(LastSessionRefreshStoreFactory.LSR_OFFLINE_PERIODIC_TASK_NAME);
+
+        return Response.noContent().build();
+    }
+
+    private void suspendTask(String taskName) {
+        TimerProvider.TimerTaskContext taskContext = session.getProvider(TimerProvider.class).cancelTask(taskName);
+
+        if (taskContext != null) {
+            suspendedTimerTasks.put(taskName, taskContext);
+        }
+    }
+
+    @POST
+    @Path("/restore-periodic-tasks")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response restorePeriodicTasks() {
+        TimerProvider timer = session.getProvider(TimerProvider.class);
+
+        for (Map.Entry<String, TimerProvider.TimerTaskContext> task : suspendedTimerTasks.entrySet()) {
+            timer.schedule(task.getValue().getRunnable(), task.getValue().getIntervalMillis(), task.getKey());
+        }
+
+        suspendedTimerTasks.clear();
+
+        return Response.noContent().build();
+    }
+
+
+    @POST
     @Path("/run-on-server")
     @Consumes(MediaType.TEXT_PLAIN_UTF_8)
     @Produces(MediaType.TEXT_PLAIN_UTF_8)
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProviderFactory.java
index 13ab66e..d796c2f 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProviderFactory.java
@@ -17,20 +17,26 @@
 
 package org.keycloak.testsuite.rest;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
 import org.keycloak.Config.Scope;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.services.resource.RealmResourceProvider;
 import org.keycloak.services.resource.RealmResourceProviderFactory;
+import org.keycloak.timer.TimerProvider;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TestingResourceProviderFactory implements RealmResourceProviderFactory {
 
+    private Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks = new ConcurrentHashMap<>();
+
     @Override
     public RealmResourceProvider create(KeycloakSession session) {
-        return new TestingResourceProvider(session);
+        return new TestingResourceProvider(session, suspendedTimerTasks);
     }
 
     @Override
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
index 080afcf..ea6fe95 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java
@@ -34,6 +34,8 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+
 import java.util.List;
 import java.util.Map;
 
@@ -180,9 +182,9 @@ public interface TestingResource {
     void removeUserSessions(@QueryParam("realm") final String realm);
 
     @GET
-    @Path("/get-user-session")
+    @Path("/get-last-session-refresh")
     @Produces(MediaType.APPLICATION_JSON)
-    Integer getLastSessionRefresh(@QueryParam("realm") final String realm, @QueryParam("session") final String sessionId);
+    Integer getLastSessionRefresh(@QueryParam("realm") final String realm, @QueryParam("session") final String sessionId, @QueryParam("offline") boolean offline);
 
     @POST
     @Path("/remove-expired")
@@ -255,6 +257,17 @@ public interface TestingResource {
     void setKrb5ConfFile(@QueryParam("krb5-conf-file") String krb5ConfFile);
 
     @POST
+    @Path("/suspend-periodic-tasks")
+    @Produces(MediaType.APPLICATION_JSON)
+    Response suspendPeriodicTasks();
+
+
+    @POST
+    @Path("/restore-periodic-tasks")
+    @Produces(MediaType.APPLICATION_JSON)
+    Response restorePeriodicTasks();
+
+    @POST
     @Path("/run-on-server")
     @Consumes(MediaType.TEXT_PLAIN_UTF_8)
     @Produces(MediaType.TEXT_PLAIN_UTF_8)
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
index f59a916..16bdaa4 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java
@@ -33,6 +33,7 @@ import org.keycloak.common.util.Time;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.events.Details;
 import org.keycloak.events.EventType;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.AccessToken;
@@ -315,7 +316,8 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         demoRealmRep.setSsoSessionIdleTimeout(1);
         testRealmResource().update(demoRealmRep);
 
-        pause(2000);
+        // Needs to add some additional time due the tolerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+        setAdapterAndServerTimeOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
 
         productPortal.navigateTo();
         assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
@@ -343,7 +345,8 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd
         demoRealmRep.setSsoSessionIdleTimeout(1);
         testRealmResource().update(demoRealmRep);
 
-        pause(2000);
+        // Needs to add some additional time due the tolerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+        setAdapterAndServerTimeOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
 
         productPortal.navigateTo();
         assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
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 2c1ca21..0ae1117 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
@@ -101,6 +101,23 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest 
 
     }
 
+    // Disable periodic tasks in cross-dc tests. It's needed to have some scenarios more stable.
+    @Before
+    public void suspendPeriodicTasks() {
+        backendTestingClients.values().stream().forEach((KeycloakTestingClient testingClient) -> {
+            testingClient.testing().suspendPeriodicTasks();
+        });
+
+    }
+
+    @After
+    public void restorePeriodicTasks() {
+        backendTestingClients.values().stream().forEach((KeycloakTestingClient testingClient) -> {
+            testingClient.testing().restorePeriodicTasks();
+        });
+    }
+
+
     @Override
     public void importTestRealms() {
         enableOnlyFirstNodeInFirstDc();
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 ddbfdb6..714c8a3 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
@@ -19,14 +19,29 @@ package org.keycloak.testsuite.crossdc;
 
 
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
+import javax.ws.rs.NotFoundException;
+
+import org.hamcrest.Matchers;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
 import org.junit.Test;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.Assert;
 import org.keycloak.common.util.Retry;
-import org.keycloak.testsuite.arquillian.ContainerInfo;
-import org.keycloak.testsuite.rest.representation.RemoteCacheStats;
+import org.keycloak.testsuite.arquillian.InfinispanStatistics;
+import org.keycloak.testsuite.arquillian.annotation.JmxInfinispanCacheStatistics;
+import org.keycloak.testsuite.client.KeycloakTestingClient;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
 import org.keycloak.testsuite.util.OAuthClient;
 
 /**
@@ -34,8 +49,40 @@ import org.keycloak.testsuite.util.OAuthClient;
  */
 public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
 
+    @Deployment(name = "dc0")
+    @TargetsContainer(QUALIFIER_AUTH_SERVER_DC_0_NODE_1)
+    public static WebArchive deployDC0() {
+        return RunOnServerDeployment.create(
+                LastSessionRefreshCrossDCTest.class,
+                AbstractAdminCrossDCTest.class,
+                AbstractCrossDCTest.class,
+                AbstractTestRealmKeycloakTest.class,
+                KeycloakTestingClient.class,
+                InfinispanStatistics.class
+        );
+    }
+
+    @Deployment(name = "dc1")
+    @TargetsContainer(QUALIFIER_AUTH_SERVER_DC_1_NODE_1)
+    public static WebArchive deployDC1() {
+        return RunOnServerDeployment.create(
+                LastSessionRefreshCrossDCTest.class,
+                AbstractAdminCrossDCTest.class,
+                AbstractCrossDCTest.class,
+                AbstractTestRealmKeycloakTest.class,
+                KeycloakTestingClient.class,
+                InfinispanStatistics.class
+        );
+    }
+
+
     @Test
-    public void testRevokeRefreshToken() {
+    public void testRevokeRefreshToken(@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc1Stats,
+                                       @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc2Stats,
+                                       @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc1Stats,
+                                       @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
+
+    ) {
         // Enable revokeRefreshToken
         RealmRepresentation realmRep = testRealm().toRepresentation();
         realmRep.setRevokeRefreshToken(true);
@@ -44,6 +91,19 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         // Enable second DC
         enableDcOnLoadBalancer(DC.SECOND);
 
+        sessionCacheDc1Stats.reset();
+        sessionCacheDc2Stats.reset();
+        clientSessionCacheDc1Stats.reset();
+        clientSessionCacheDc2Stats.reset();
+
+        // Get statistics
+        AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
+        AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
+        AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
+        AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
+        AtomicInteger lsrDc1 = new AtomicInteger(-1);
+        AtomicInteger lsrDc2 = new AtomicInteger(-1);
+
         // Login
         OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
         String code = response1.getCode();
@@ -53,44 +113,33 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         String refreshToken1 = tokenResponse.getRefreshToken();
 
 
-        // 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.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
-        log.infof("lsr00: %d, lsr10: %d, lsrr0: %d", lsr00, lsr10, lsrr0);
-
-        Assert.assertEquals(lsr00, lsr10);
-        Assert.assertEquals(lsr00, lsrr0);
+        // Assert statistics - sessions created on both DCs and created on remoteCaches too
+        assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, false);
 
 
         // Set time offset to some point in future. TODO This won't be needed once we have single-use cache based solution for refresh tokens
         setTimeOffset(10);
 
-        // refresh token on DC0
+        // refresh token on DC1
         disableDcOnLoadBalancer(DC.SECOND);
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
         String refreshToken2 = tokenResponse.getRefreshToken();
 
-        // Assert times changed on DC0, DC1 and remoteCache
-        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.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
-            log.infof("lsr01: %d, lsr11: %d, lsrr1: %d", lsr01, lsr11, lsrr1);
-            
-            Assert.assertEquals(lsr01, lsr11);
-            Assert.assertEquals(lsr01, lsrr1);
-            Assert.assertTrue(lsr01 > lsr00);
-        }, 50, 50);
+        // Assert statistics - sessions updated on both DCs and on remoteCaches too
+        assertStatistics("After time offset 10", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, false);
 
-        // try refresh with old token on DC1. It should fail.
+        // try refresh with old token on DC2. It should fail.
         disableDcOnLoadBalancer(DC.FIRST);
         enableDcOnLoadBalancer(DC.SECOND);
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
         Assert.assertNull("Expecting no access token present", tokenResponse.getAccessToken());
         Assert.assertNotNull(tokenResponse.getError());
 
-        // try refresh with new token on DC1. It should pass.
+        // try refresh with new token on DC2. It should pass.
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken2, "password");
         Assert.assertNotNull(tokenResponse.getAccessToken());
         Assert.assertNull(tokenResponse.getError());
@@ -103,12 +152,35 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
 
 
     @Test
-    public void testLastSessionRefreshUpdate() {
-        // Disable DC1 on loadbalancer
+    public void testLastSessionRefreshUpdate(@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc1Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc2Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc1Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
+
+    ) {
+
+        // TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
+
+        // Ensure to remove all current sessions and offline sessions
+        setTimeOffset(10000000);
+        getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
+        setTimeOffset(0);
+
+        sessionCacheDc1Stats.reset();
+        sessionCacheDc2Stats.reset();
+        clientSessionCacheDc1Stats.reset();
+        clientSessionCacheDc2Stats.reset();
+
+        // Disable DC2 on loadbalancer
         disableDcOnLoadBalancer(DC.SECOND);
 
         // Get statistics
-        int stores0 = getRemoteCacheStats(0).getGlobalStores();
+        AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
+        AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
+        AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
+        AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
+        AtomicInteger lsrDc1 = new AtomicInteger(-1);
+        AtomicInteger lsrDc2 = new AtomicInteger(-1);
 
         // Login
         OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
@@ -118,121 +190,264 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
         String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
         String refreshToken1 = tokenResponse.getRefreshToken();
 
+        // Assert statistics - sessions created on both DCs and created on remoteCaches too
+        assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, false);
 
-        // Get statistics
-        this.suiteContext.getDcAuthServerBackendsInfo().get(0).stream()
-                .filter(ContainerInfo::isStarted).findFirst().get();
 
-        AtomicInteger stores1 = new AtomicInteger(-1);
-        Retry.execute(() -> {
-            stores1.set(getRemoteCacheStats(0).getGlobalStores());
-            log.infof("stores0=%d, stores1=%d", stores0, stores1.get());
-            Assert.assertTrue(stores1.get() > stores0);
-        }, 50, 50);
+        // Set time offset
+        setTimeOffset(100);
 
-        int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId);
-        int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId);
-        Assert.assertEquals(lsr00, lsr10);
+        // refresh token on DC1
+        tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+        String refreshToken3 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken3);
+
+        // Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
+        assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, false, false);
 
-        // Set time offset to some point in future.
-        setTimeOffset(10);
 
-        // refresh token on DC0
+        // Set time offset
+        setTimeOffset(110);
+
+        // refresh token on DC1
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
         String refreshToken2 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken2);
 
-        // assert that hotrod statistics were NOT updated
-        AtomicInteger stores2 = new AtomicInteger(-1);
+        // Assert statistics - sessions updated just on DC1.
+        // Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
+        assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, false, false, false);
 
-        // TODO: not sure why stores2 < stores1 at first run. Probably should be replaced with JMX statistics
-        Retry.execute(() -> {
-            stores2.set(getRemoteCacheStats(0).getGlobalStores());
-            log.infof("stores1=%d, stores2=%d", stores1.get(), stores2.get());
-            Assert.assertEquals(stores1.get(), stores2.get());
-        }, 50, 50);
 
-        // assert that lastSessionRefresh on DC0 updated, but on DC1 still the same
-        AtomicInteger lsr01 = new AtomicInteger(-1);
-        AtomicInteger lsr11 = new AtomicInteger(-1);
-        Retry.execute(() -> {
-            lsr01.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
-            lsr11.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
-            log.infof("lsr01: %d, lsr11: %d", lsr01.get(), lsr11.get());
-            Assert.assertTrue(lsr01.get() > lsr00);
-        }, 50, 100);
-        Assert.assertEquals(lsr10, lsr11.get());
-
-        // assert that lastSessionRefresh still the same on remoteCache
-        int lsrr1 = getTestingClientForStartedNodeInDc(0).testing("test").cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME).getRemoteCacheLastSessionRefresh(sessionId);
-        Assert.assertEquals(lsr00, lsrr1);
-        log.infof("lsrr1: %d", lsrr1);
-
-        // setTimeOffset to greater value
+        // 31 minutes after "100". Session should be still valid and not yet expired (RefreshToken will be invalid due the expiration on the JWT itself. Hence not testing refresh here)
+        setTimeOffset(1960);
+
+        boolean sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
+            return AuthenticationManager.isSessionValid(realm, userSession);
+        }, Boolean.class);
+
+        Assert.assertTrue(sessionValid);
+
+        getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
+
+        // Assert statistics - nothing was updated. No refresh happened and nothing was cleared during "removeExpired"
+        assertStatistics("After checking valid at time 1960", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, false, false, false, false);
+
+
+        // 35 minutes after "100". Session not valid and will be expired by the cleaner
+        setTimeOffset(2200);
+
+        sessionValid = getTestingClientForStartedNodeInDc(1).server("test").fetch((KeycloakSession session) -> {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
+            return AuthenticationManager.isSessionValid(realm, userSession);
+        }, Boolean.class);
+
+        Assert.assertFalse(sessionValid);
+
+        getTestingClientForStartedNodeInDc(1).testing("test").removeExpired("test");
+
+        // Session should be removed on both DCs
+        try {
+            getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, false);
+            Assert.fail("It wasn't expected to find the session " + sessionId);
+        } catch (NotFoundException nfe) {
+            // Expected
+        }
+        try {
+            getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, false);
+            Assert.fail("It wasn't expected to find the session " + sessionId);
+        } catch (NotFoundException nfe) {
+            // Expected
+        }
+    }
+
+
+    @Test
+    public void testOfflineSessionsLastSessionRefreshUpdate(@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc1Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics sessionCacheDc2Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc1Stats,
+                                             @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientSessionCacheDc2Stats
+
+    ) throws Exception {
+
+        // TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
+
+        // Ensure to remove all current sessions and offline sessions
+        setTimeOffset(10000000);
+        getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
+        setTimeOffset(0);
+
+        sessionCacheDc1Stats.reset();
+        sessionCacheDc2Stats.reset();
+        clientSessionCacheDc1Stats.reset();
+        clientSessionCacheDc2Stats.reset();
+
+        // Disable DC2 on loadbalancer
+        disableDcOnLoadBalancer(DC.SECOND);
+
+        // Get statistics
+        AtomicLong sessionStoresDc1 = new AtomicLong(getStores(sessionCacheDc1Stats));
+        AtomicLong sessionStoresDc2 = new AtomicLong(getStores(sessionCacheDc2Stats));
+        AtomicLong clientSessionStoresDc1 = new AtomicLong(getStores(clientSessionCacheDc1Stats));
+        AtomicLong clientSessionStoresDc2 = new AtomicLong(getStores(clientSessionCacheDc2Stats));
+        AtomicInteger lsrDc1 = new AtomicInteger(-1);
+        AtomicInteger lsrDc2 = new AtomicInteger(-1);
+
+        // Login
+        oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
+        OAuthClient.AuthorizationEndpointResponse response1 = oauth.doLogin("test-user@localhost", "password");
+        String code = response1.getCode();
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+        Assert.assertNotNull(tokenResponse.getAccessToken());
+        String sessionId = oauth.verifyToken(tokenResponse.getAccessToken()).getSessionState();
+        String refreshToken1 = tokenResponse.getRefreshToken();
+
+        // Assert statistics - sessions created on both DCs and created on remoteCaches too
+        assertStatistics("After session created", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, true);
+
+
+        // Set time offset
         setTimeOffset(100);
 
-        // refresh token
+        // refresh token on DC1
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+        String refreshToken3 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken3);
 
-        // assert that lastSessionRefresh on both DC0 and DC1 was updated, but on remoteCache still the same
-        AtomicInteger lsr02 = new AtomicInteger(-1);
-        AtomicInteger lsr12 = new AtomicInteger(-1);
-        AtomicInteger lsrr2 = new AtomicInteger(-1);
-        AtomicInteger stores3 = new AtomicInteger(-1);
-        Retry.execute(() -> {
-            lsr02.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
-            lsr12.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
-            log.infof("lsr02: %d, lsr12: %d", lsr02.get(), lsr12.get());
-            Assert.assertEquals(lsr02.get(), lsr12.get());
-            Assert.assertTrue(lsr02.get() > lsr01.get());
-            Assert.assertTrue(lsr12.get() > lsr11.get());
-
-            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());
-
-            // assert that hotrod statistics were NOT updated on DC0
-            stores3.set(getRemoteCacheStats(0).getGlobalStores());
-            log.infof("stores2=%d, stores3=%d", stores2.get(), stores3.get());
-            Assert.assertEquals(stores2.get(), stores3.get());
-        }, 50, 100);
-
-        // Increase time offset even more
-        setTimeOffset(1500);
-
-        // refresh token
+        // Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches not updated
+        assertStatistics("After refresh at time 100", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, false, true);
+
+
+
+        // Set time offset
+        setTimeOffset(110);
+
+        // refresh token on DC1
         tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
-        Assert.assertNull("Error: " + tokenResponse.getError() + ", error description: " + tokenResponse.getErrorDescription(), tokenResponse.getError());
-        Assert.assertNotNull(tokenResponse.getRefreshToken());
-
-        // assert that lastSessionRefresh updated everywhere including remoteCache
-        AtomicInteger lsr03 = new AtomicInteger(-1);
-        AtomicInteger lsr13 = new AtomicInteger(-1);
-        AtomicInteger lsrr3 = new AtomicInteger(-1);
-        AtomicInteger stores4 = new AtomicInteger(-1);
-        Retry.execute(() -> {
-            lsr03.set(getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId));
-            lsr13.set(getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId));
-            log.infof("lsr03: %d, lsr13: %d", lsr03.get(), lsr13.get());
-            Assert.assertEquals(lsr03.get(), lsr13.get());
-            Assert.assertTrue(lsr03.get() > lsr02.get());
-            Assert.assertTrue(lsr13.get() > lsr12.get());
-
-            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());
-
-            // assert that hotrod statistics were NOT updated on DC0
-            stores4.set(getRemoteCacheStats(0).getGlobalStores());
-            log.infof("stores3=%d, stores4=%d", stores3.get(), stores4.get());
-            Assert.assertTrue(stores4.get() > stores3.get());
-        }, 50, 100);
+        String refreshToken2 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken2);
+
+        // Assert statistics - sessions updated just on DC1.
+        // Update of DC2 is postponed (It's just 10 seconds since last message). RemoteCaches not updated
+        assertStatistics("After refresh at time 110", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, false, false, true);
+
+
+        // Set time offset to 20 days
+        setTimeOffset(1728000);
+
+        // refresh token on DC1
+        tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+        String refreshToken4 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken4);
+
+        // Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches updated as well.
+        assertStatistics("After refresh at time 1728000", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, true);
+
+        // Set time offset to 30 days
+        setTimeOffset(2592000);
+
+        // refresh token on DC1
+        tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+        String refreshToken5 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken5);
+
+        // Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches won't be updated now due it's just 10 days from the last remoteCache update
+        assertStatistics("After refresh at time 2592000", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, false, true);
+
+        // Set time offset to 40 days
+        setTimeOffset(3456000);
+
+        // refresh token on DC1
+        tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password");
+        String refreshToken6 = tokenResponse.getRefreshToken();
+        Assert.assertNotNull(refreshToken6);
+
+        // Assert statistics - sessions updated on both DC1 and DC2. RemoteCaches will be updated too due it's 20 days from the last remoteCache update
+        assertStatistics("After refresh at time 3456000", sessionId, sessionCacheDc1Stats, sessionCacheDc2Stats, clientSessionCacheDc1Stats, clientSessionCacheDc2Stats,
+                sessionStoresDc1, sessionStoresDc2, clientSessionStoresDc1, clientSessionStoresDc2,
+                lsrDc1, lsrDc2, true, true, true, true);
+
     }
 
 
-    private RemoteCacheStats getRemoteCacheStats(int dcIndex) {
-        return getTestingClientForStartedNodeInDc(dcIndex).testing("test")
-                .cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME)
-                .getRemoteCacheStats();
+    private void assertStatistics(String messagePrefix, String sessionId,
+                                  InfinispanStatistics sessionCacheDc1Stats, InfinispanStatistics sessionCacheDc2Stats, InfinispanStatistics clientSessionCacheDc1Stats, InfinispanStatistics clientSessionCacheDc2Stats,
+                                  AtomicLong sessionStoresDc1, AtomicLong sessionStoresDc2, AtomicLong clientSessionStoresDc1, AtomicLong clientSessionStoresDc2,
+                                  AtomicInteger lsrDc1, AtomicInteger lsrDc2,
+                                  boolean expectedUpdatedLsrDc1, boolean expectedUpdatedLsrDc2, boolean expectedUpdatedRemoteCache, boolean offline) {
+        Retry.execute(() -> {
+            long newSessionStoresDc1 = getStores(sessionCacheDc1Stats);
+            long newSessionStoresDc2 = getStores(sessionCacheDc2Stats);
+            long newClientSessionStoresDc1 = getStores(clientSessionCacheDc1Stats);
+            long newClientSessionStoresDc2 = getStores(clientSessionCacheDc2Stats);
+
+            int newLsrDc1 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId, offline);
+            int newLsrDc2 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId, offline);
+
+            log.infof(messagePrefix + ": sessionStoresDc1: %d, sessionStoresDc2: %d, clientSessionStoresDc1: %d, clientSessionStoresDc2: %d, lsrDc1: %d, lsrDc2: %d",
+                    newSessionStoresDc1, newSessionStoresDc2, newClientSessionStoresDc1, newClientSessionStoresDc2, newLsrDc1, newLsrDc2);
+
+            // Check lastSessionRefresh updated on DC1
+            if (expectedUpdatedLsrDc1) {
+                Assert.assertThat(newLsrDc1, Matchers.greaterThan(lsrDc1.get()));
+            } else {
+                Assert.assertEquals(newLsrDc1, lsrDc1.get());
+            }
+
+            // Check lastSessionRefresh updated on DC2
+            if (expectedUpdatedLsrDc2) {
+                Assert.assertThat(newLsrDc2, Matchers.greaterThan(lsrDc2.get()));
+            } else {
+                Assert.assertEquals(newLsrDc2, lsrDc2.get());
+            }
+
+            // Check store statistics updated on JDG side
+            if (expectedUpdatedRemoteCache) {
+                Assert.assertThat(newSessionStoresDc1, Matchers.greaterThan(sessionStoresDc1.get()));
+                Assert.assertThat(newSessionStoresDc2, Matchers.greaterThan(sessionStoresDc2.get()));
+                Assert.assertThat(newClientSessionStoresDc1, Matchers.greaterThan(clientSessionStoresDc1.get()));
+                Assert.assertThat(newClientSessionStoresDc2, Matchers.greaterThan(clientSessionStoresDc2.get()));
+            } else {
+                Assert.assertEquals(newSessionStoresDc1, sessionStoresDc1.get());
+                Assert.assertEquals(newSessionStoresDc2, sessionStoresDc2.get());
+                Assert.assertEquals(newClientSessionStoresDc1, clientSessionStoresDc1.get());
+                Assert.assertEquals(newClientSessionStoresDc2, clientSessionStoresDc2.get());
+            }
+
+            // Update counter references
+            sessionStoresDc1.set(newSessionStoresDc1);
+            sessionStoresDc2.set(newSessionStoresDc2);
+            clientSessionStoresDc1.set(newClientSessionStoresDc1);
+            clientSessionStoresDc2.set(newClientSessionStoresDc2);
+            lsrDc1.set(newLsrDc1);
+            lsrDc2.set(newLsrDc2);
+        }, 50, 50);
+
+    }
+
+    private long getStores(InfinispanStatistics cacheStats) {
+        return (long) cacheStats.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_STORES);
     }
 
 }
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 b62404f..4099d27 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
@@ -59,6 +59,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     private int sessions01;
     private int sessions02;
+    private int clientSessions01;
+    private int clientSessions02;
     private int remoteSessions01;
     private int remoteSessions02;
 
@@ -102,12 +104,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testRealmRemoveSessions(
-            @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,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -118,13 +120,13 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).remove();
 
         // Assert sessions removed on node1 and node2 and on remote caches
-        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     // Return last used accessTokenResponse
-    private List<OAuthClient.AccessTokenResponse> createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception {
+    private List<OAuthClient.AccessTokenResponse> createInitialSessions(String cacheName, String clientSessionsCacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception {
 
         // Enable second DC
         enableDcOnLoadBalancer(DC.SECOND);
@@ -132,9 +134,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         // Check sessions count before test
         sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
         sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+        clientSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
+        clientSessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
         remoteSessions01 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
         remoteSessions02 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
-        log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d", sessions01, sessions02, remoteSessions01, remoteSessions02);
+        log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d, clientSessions01: %d, clientSessions02: %d",
+                sessions01, sessions02, remoteSessions01, remoteSessions02, clientSessions01, clientSessions02);
 
         // Create 20 user sessions
         oauth.realm(REALM_NAME);
@@ -152,9 +157,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         Retry.execute(() -> {
             int sessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
             int sessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+            int clientSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
+            int clientSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
             int remoteSessions11 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
             int remoteSessions12 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
-            log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d", sessions11, sessions12, remoteSessions11, remoteSessions12);
+            log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d, clientSessions11: %d, clientSessions12: %d",
+                    sessions11, sessions12, remoteSessions11, remoteSessions12, clientSessions11, clientSessions12);
 
             Assert.assertEquals(sessions11, sessions01 + SESSIONS_COUNT);
             Assert.assertEquals(sessions12, sessions02 + SESSIONS_COUNT);
@@ -169,11 +177,14 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
     }
 
 
-    private void assertStatisticsExpected(String messagePrefix, String cacheName, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc,
-                                  int sessions1Expected, int sessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, boolean checkSomeMessagesSentBetweenDCs) {
+    private void assertStatisticsExpected(String messagePrefix, String cacheName, String clientSessionsCacheName,
+                                          InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc,
+                                  int sessions1Expected, int sessions2Expected, int clientSessions1Expected, int clientSessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, boolean checkSomeMessagesSentBetweenDCs) {
         Retry.execute(() -> {
             int sessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
             int sessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
+            int clientSessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
+            int clientSessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
             int remoteSessions1 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
             int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
             long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES);
@@ -181,6 +192,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
             Assert.assertEquals(sessions1, sessions1Expected);
             Assert.assertEquals(sessions2, sessions2Expected);
+            Assert.assertEquals(clientSessions1, clientSessions1Expected);
+            Assert.assertEquals(clientSessions2, clientSessions2Expected);
             Assert.assertEquals(remoteSessions1, remoteSessions1Expected);
             Assert.assertEquals(remoteSessions2, remoteSessions2Expected);
 
@@ -195,11 +208,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testRealmRemoveOfflineSessions(
-            @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,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,true, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -207,18 +220,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).remove();
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     @Test
     public void testLogoutAllInRealm(
-            @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, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -226,18 +239,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).logoutAll();
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     @Test
-    public void testPeriodicExpiration(
-            @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,
+    public void testPeriodicExpirationSessions(
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
+        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_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");
@@ -250,8 +263,9 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
 
         // Nothing yet expired. It may happen that no message sent between DCs
-        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, false);
+        assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT,  clientSessions01 + SESSIONS_COUNT, clientSessions02 + SESSIONS_COUNT,
+                remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
 
 
         // Set time offset
@@ -269,8 +283,55 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
+    }
+
+
+    @Test
+    public void testPeriodicExpirationOfflineSessions(
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
+
+        OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
+                true, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
+
+        // Assert I am able to refresh
+        OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
+        Assert.assertNotNull(refreshResponse.getRefreshToken());
+        Assert.assertNull(refreshResponse.getError());
+
+        channelStatisticsCrossDc.reset();
+
+        // Remove expired in DC0
+        getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+
+        // Nothing yet expired. It may happen that no message sent between DCs
+        assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
+                cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT,  clientSessions01 + SESSIONS_COUNT, clientSessions02 + SESSIONS_COUNT,
+                remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
+
+
+        // Set time offset
+        setTimeOffset(10000000);
+
+        // Assert I am not able to refresh anymore
+        refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
+        Assert.assertNull(refreshResponse.getRefreshToken());
+        Assert.assertNotNull(refreshResponse.getError());
+
+
+        channelStatisticsCrossDc.reset();
+
+        // Remove expired in DC0
+        getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
+
+        // Assert sessions removed on node1 and node2 and on remote caches.
+        assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
+                cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
@@ -278,10 +339,10 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testUserRemoveSessions(
-            @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, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,false, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -293,17 +354,17 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     @Test
     public void testUserRemoveOfflineSessions(
-            @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,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
-        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,true, cacheDc1Statistics, cacheDc2Statistics, true);
 
 //        log.infof("Sleeping!");
 //        Thread.sleep(10000000);
@@ -315,18 +376,19 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
+                cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     @Test
     public void testLogoutUser(
-            @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, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
 
-        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
+        createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
 
         channelStatisticsCrossDc.reset();
 
@@ -336,29 +398,33 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
         getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId());
 
         // Just one session expired.
-        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, true);
+        assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
+                cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01 + SESSIONS_COUNT - 1, sessions02 + SESSIONS_COUNT - 1, clientSessions01 + SESSIONS_COUNT - 1, clientSessions02 + SESSIONS_COUNT - 1,
+                remoteSessions01 + SESSIONS_COUNT - 1, remoteSessions02 + SESSIONS_COUNT - 1, true);
 
         // Logout all sessions for user now
         user.logout();
 
         // Assert sessions removed on node1 and node2 and on remote caches.
-        assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
-                sessions01, sessions02, remoteSessions01, remoteSessions02, true);
+        assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
+                cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
+                sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
     }
 
 
     @Test
     public void testLogoutUserWithFailover(
-            @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, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", 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.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false);
+        List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_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);
@@ -396,8 +462,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
 
     @Test
     public void testPeriodicExpirationAuthSessions(
-            @JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
-            @JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
+            @JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
             @JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
         createInitialAuthSessions();
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index c8b320d..ae0b447 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -25,6 +25,7 @@ import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.common.enums.SslRequired;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.RefreshToken;
@@ -533,7 +534,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
 
         String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
 
-        int last = testingClient.testing().getLastSessionRefresh("test", sessionId);
+        int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
 
         setTimeOffset(2);
 
@@ -544,7 +545,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
 
         assertEquals(200, tokenResponse.getStatusCode());
 
-        int next = testingClient.testing().getLastSessionRefresh("test", sessionId);
+        int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
 
         Assert.assertNotEquals(last, next);
 
@@ -555,7 +556,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
         setTimeOffset(4);
         tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
 
-        next = testingClient.testing().getLastSessionRefresh("test", sessionId);
+        next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
 
         // lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout
         Assert.assertThat(next, allOf(greaterThan(last), lessThan(last + 50)));
@@ -564,7 +565,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
         RealmManager.realm(realmResource).ssoSessionIdleTimeout(1);
 
         events.clear();
-        setTimeOffset(6);
+        // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+        setTimeOffset(6 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
         tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
 
         // test idle timeout
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index 98215f7..b99ed06 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -32,6 +32,7 @@ import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.SessionTimeoutHelper;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
 import org.keycloak.representations.VersionRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
@@ -315,8 +316,8 @@ public class AdapterTestStrategy extends ExternalResource {
         session.getTransactionManager().commit();
         session.close();
 
-        Time.setOffset(2);
-
+        // Needs to add some additional time due the tolerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+        Time.setOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
 
         // test SSO
         driver.navigate().to(APP_SERVER_BASE_URL + "/product-portal");
@@ -350,7 +351,8 @@ public class AdapterTestStrategy extends ExternalResource {
         session.getTransactionManager().commit();
         session.close();
 
-        Time.setOffset(2);
+        // Needs to add some additional time due the tolerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+        Time.setOffset(2 + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS);
 
         session = keycloakRule.startSession();
         realm = session.realms().getRealmByName("demo");