keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 6(+0 -6)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java 28(+23 -5)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java 92(+73 -19)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java 4(+2 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshStoreFactory.java 13(+9 -4)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java 32(+32 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 2(+1 -1)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 33(+19 -14)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 6(+4 -2)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java 10(+2 -8)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 2(+1 -1)
model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java 2(+1 -1)
model/infinispan/src/test/java/org/keycloak/keys/infinispan/InfinispanKeyStorageProviderTest.java 3(+2 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 51(+46 -5)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProviderFactory.java 8(+7 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestingResource.java 17(+15 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java 7(+5 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java 17(+17 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/LastSessionRefreshCrossDCTest.java 465(+340 -125)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/SessionExpirationCrossDCTest.java 170(+118 -52)
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");