From bd1072d2eba24402350a9e386de9d1dc44c21225 Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 27 Oct 2017 16:14:08 +0200 Subject: [PATCH] KEYCLOAK-5747 Ensure refreshToken doesn't need to send request to the other DC. Other fixes and polishing --- ...ltInfinispanConnectionProviderFactory.java | 6 - .../AuthenticatedClientSessionAdapter.java | 28 +- .../InfinispanUserSessionProvider.java | 33 +- .../InfinispanUserSessionProviderFactory.java | 6 +- .../infinispan/UserSessionAdapter.java | 2 +- .../sessions/LastSessionRefreshChecker.java | 92 +++- .../sessions/LastSessionRefreshListener.java | 4 +- .../LastSessionRefreshStoreFactory.java | 13 +- .../AuthenticatedClientSessionEntity.java | 32 ++ .../entities/UserSessionEntity.java | 2 +- .../remotestore/RemoteCacheInvoker.java | 10 +- .../ConcurrencyJDGSessionsCacheTest.java | 2 +- .../InfinispanKeyStorageProviderTest.java | 3 +- .../models/utils/SessionTimeoutHelper.java | 55 ++ .../org/keycloak/timer/TimerProvider.java | 17 +- .../keycloak/protocol/oidc/TokenManager.java | 7 +- .../managers/AuthenticationManager.java | 13 +- .../resources/KeycloakApplication.java | 2 +- .../scheduled/ClearExpiredUserSessions.java | 2 + .../timer/basic/BasicTimerProvider.java | 13 +- .../basic/BasicTimerProviderFactory.java | 6 +- .../timer/basic/TimerTaskContextImpl.java | 48 ++ .../rest/TestingResourceProvider.java | 51 +- .../rest/TestingResourceProviderFactory.java | 8 +- .../client/resources/TestingResource.java | 17 +- .../AbstractDemoServletsAdapterTest.java | 7 +- .../crossdc/AbstractCrossDCTest.java | 17 + .../LastSessionRefreshCrossDCTest.java | 469 +++++++++++++----- .../crossdc/SessionExpirationCrossDCTest.java | 170 +++++-- .../testsuite/oauth/RefreshTokenTest.java | 10 +- .../adapter/AdapterTestStrategy.java | 8 +- 31 files changed, 876 insertions(+), 277 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/models/utils/SessionTimeoutHelper.java create mode 100644 services/src/main/java/org/keycloak/timer/basic/TimerTaskContextImpl.java 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 572c5f0a17..7493cae08b 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 736e7568cb..cf8f57746f 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 userSessionUpdateTx; private final InfinispanChangelogBasedTransaction 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 userSessionUpdateTx, - InfinispanChangelogBasedTransaction clientSessionUpdateTx) { + InfinispanChangelogBasedTransaction 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 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/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 2c0411ec48..5015130785 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 userSessionUpdateTx = getTransaction(false); InfinispanChangelogBasedTransaction 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 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> localCache = CacheDecorators.localCache(sessionCache); - Cache> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache); + Cache> localClientSessionCache = CacheDecorators.localCache(clientSessionCache); Cache> 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> localCache = CacheDecorators.localCache(offlineSessionCache); @@ -501,6 +502,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { Cache> 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 userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction 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 userSessionUpdateTx = getTransaction(true); InfinispanChangelogBasedTransaction 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 userSessionUpdateTx, - InfinispanChangelogBasedTransaction clientSessionUpdateTx) { + InfinispanChangelogBasedTransaction 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 { 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 22b7382383..697c3f2f5d 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> 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> 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> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME); 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 de825573fe..d0c2a4a804 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 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/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 25e0df942f..6d2e8e2133 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 sessionWrapper, boolean offline, int newLastSessionRefresh) { + public SessionUpdateTask.CrossDCMessageStatus shouldSaveUserSessionToRemoteCache( + KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper 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 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 892ecfe973..00b499e974 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 d7b85590f5..6db17d0760 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> 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> 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 18d892f233..16ce4abf68 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 { @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 dbde092725..fafd1561b8 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/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java index 404d3ca6e8..e32b919f62 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 Marek Posolda @@ -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 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/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java b/model/infinispan/src/test/java/org/keycloak/cluster/infinispan/ConcurrencyJDGSessionsCacheTest.java index 0d28458ec8..2e4428a941 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> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME); Cache> 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 030e5a0f33..a9da1d7b5f 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 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 0000000000..b52d185822 --- /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 Marek Posolda + */ +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 5dbf69bff9..7b27941dbf 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 d0774c48e7..47f44e57ed 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 32f864592c..8791010931 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 ae0979da4f..d4e2905ae1 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 5315be4add..e61969ad8f 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 29a736f47d..73f38be902 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 ea0da94b22..06559bcc41 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 scheduledTasks = new ConcurrentHashMap(); + private ConcurrentMap 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 0000000000..c53cb8769b --- /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 Marek Posolda + */ +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 697731021e..953cb95fea 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 Stian Thorgersen */ public class TestingResourceProvider implements RealmResourceProvider { - private KeycloakSession session; + private final KeycloakSession session; + private final Map suspendedTimerTasks; @Override public Object getResource() { return this; } - public TestingResourceProvider(KeycloakSession session) { + public TestingResourceProvider(KeycloakSession session, Map 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"); } @@ -673,6 +679,41 @@ public class TestingResourceProvider implements RealmResourceProvider { System.setProperty("java.security.krb5.conf", krb5ConfFile); } + @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 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) 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 13ab66e9c3..d796c2fab9 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 Stian Thorgersen */ public class TestingResourceProviderFactory implements RealmResourceProviderFactory { + private Map 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 080afcf769..ea6fe957fe 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") @@ -254,6 +256,17 @@ public interface TestingResource { @Consumes(MediaType.APPLICATION_JSON) 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) 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 f59a916b2f..16bdaa4a51 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 2c1ca21971..0ae1117f73 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 ddbfdb6070..714c8a3275 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); - - int lsr00 = getTestingClientForStartedNodeInDc(0).testing("test").getLastSessionRefresh("test", sessionId); - int lsr10 = getTestingClientForStartedNodeInDc(1).testing("test").getLastSessionRefresh("test", sessionId); - Assert.assertEquals(lsr00, lsr10); - - // Set time offset to some point in future. - setTimeOffset(10); - - // refresh token on DC0 - tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); - String refreshToken2 = tokenResponse.getRefreshToken(); - - // assert that hotrod statistics were NOT updated - AtomicInteger stores2 = new AtomicInteger(-1); - - // 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 + // 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()); + // 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); - 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); + // Set time offset + setTimeOffset(110); - // Increase time offset even more - setTimeOffset(1500); - - // refresh token + // refresh token on DC1 tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); - Assert.assertNull("Error: " + tokenResponse.getError() + ", error description: " + tokenResponse.getErrorDescription(), tokenResponse.getError()); - Assert.assertNotNull(tokenResponse.getRefreshToken()); + String refreshToken2 = tokenResponse.getRefreshToken(); + Assert.assertNotNull(refreshToken2); - // 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()); + // 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); - 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); + // 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 + } } - private RemoteCacheStats getRemoteCacheStats(int dcIndex) { - return getTestingClientForStartedNodeInDc(dcIndex).testing("test") - .cache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) - .getRemoteCacheStats(); + @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 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, true); + + + + // Set time offset + setTimeOffset(110); + + // refresh token on DC1 + tokenResponse = oauth.doRefreshTokenRequest(refreshToken1, "password"); + 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 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 b62404f52b..4099d276c5 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 createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception { + private List 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 responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false); + List 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 c8b320d18d..ae0b447a02 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 98215f7bfb..b99ed06c49 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");