KEYCLOAK-5747 Ensure refreshToken doesn't need to send request to the other DC. Other fixes and polishing

This commit is contained in:
mposolda 2017-10-27 16:14:08 +02:00 committed by Hynek Mlnařík
parent 61c5a332b4
commit bd1072d2eb
31 changed files with 876 additions and 277 deletions

View file

@ -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

View file

@ -24,16 +24,16 @@ import java.util.Set;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.ClientSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus;
import org.keycloak.models.sessions.infinispan.changes.Tasks;
import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.util.UUID;
@ -43,25 +43,31 @@ import java.util.UUID;
*/
public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
private final KeycloakSession kcSession;
private final InfinispanUserSessionProvider provider;
private AuthenticatedClientSessionEntity entity;
private final ClientModel client;
private final InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx;
private final InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx;
private UserSessionModel userSession;
private boolean offline;
public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client,
UserSessionModel userSession,
public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, InfinispanUserSessionProvider provider,
AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession,
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx, boolean offline) {
if (userSession == null) {
throw new NullPointerException("userSession must not be null");
}
this.kcSession = kcSession;
this.provider = provider;
this.entity = entity;
this.userSession = userSession;
this.client = client;
this.userSessionUpdateTx = userSessionUpdateTx;
this.clientSessionUpdateTx = clientSessionUpdateTx;
this.offline = offline;
}
private void update(UserSessionUpdateTask task) {
@ -141,6 +147,18 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
public void runUpdate(AuthenticatedClientSessionEntity entity) {
entity.setTimestamp(timestamp);
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<AuthenticatedClientSessionEntity> sessionWrapper) {
return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
.shouldSaveClientSessionToRemoteCache(kcSession, client.getRealm(), sessionWrapper, userSession, offline, timestamp);
}
@Override
public String toString() {
return "setTimestamp(" + timestamp + ')';
}
};
update(task);

View file

@ -38,8 +38,6 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CacheOperation;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask.CrossDCMessageStatus;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
@ -56,6 +54,7 @@ import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.SessionTimeoutHelper;
import java.util.Iterator;
import java.util.LinkedList;
@ -164,8 +163,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(false);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession,
userSessionUpdateTx, clientSessionUpdateTx);
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, false);
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity);
@ -446,17 +444,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private void removeExpiredUserSessions(RealmModel realm) {
int expired = Time.currentTime() - realm.getSsoSessionMaxLifespan();
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
FuturesHelper futures = new FuturesHelper();
// Each cluster node cleanups just local sessions, which are those owned by itself (+ few more taking l1 cache into account)
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(offlineClientSessionCache);
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> localClientSessionCache = CacheDecorators.localCache(clientSessionCache);
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
final AtomicInteger userSessionsSize = new AtomicInteger();
final AtomicInteger clientSessionsSize = new AtomicInteger();
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
localCacheStoreIgnore
@ -474,6 +473,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
futures.addTask(future);
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
clientSessionsSize.incrementAndGet();
Future f = localClientSessionCache.removeAsync(clientSessionId);
futures.addTask(f);
});
@ -483,12 +483,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
futures.waitForAllToFinish();
log.debugf("Removed %d expired user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
log.debugf("Removed %d expired user sessions and %d expired client sessions for realm '%s'", userSessionsSize.get(),
clientSessionsSize.get(), realm.getName());
}
private void removeExpiredOfflineUserSessions(RealmModel realm) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout() - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
@ -501,6 +502,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
final AtomicInteger userSessionsSize = new AtomicInteger();
final AtomicInteger clientSessionsSize = new AtomicInteger();
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
localCacheStoreIgnore
@ -517,6 +519,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
Future future = localCache.removeAsync(userSessionEntity.getId());
futures.addTask(future);
userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> {
clientSessionsSize.incrementAndGet();
Future f = localClientSessionCache.removeAsync(clientSessionId);
futures.addTask(f);
});
@ -533,7 +536,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
futures.waitForAllToFinish();
log.debugf("Removed %d expired offline user sessions for realm '%s'", userSessionsSize.get(), realm.getName());
log.debugf("Removed %d expired offline user sessions and %d expired offline client sessions for realm '%s'",
userSessionsSize.get(), clientSessionsSize.get(), realm.getName());
}
@Override
@ -712,7 +716,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(offline);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
return entity != null ? new AuthenticatedClientSessionAdapter(entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx) : null;
return entity != null ? new AuthenticatedClientSessionAdapter(session,this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, offline) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@ -762,7 +766,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx = getTransaction(true);
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(true);
AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true);
// update timestamp to current time
offlineClientSession.setTimestamp(Time.currentTime());
@ -831,7 +835,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
// Handle client sessions
if (importAuthenticatedClientSessions) {
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx);
importClientSession(importedSession, clientSession, userSessionUpdateTx, clientSessionUpdateTx, offline);
}
}
@ -841,7 +845,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession,
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx) {
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
boolean offline) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setRealmId(sessionToImportInto.getRealm().getId());
final UUID clientSessionId = entity.getId();
@ -864,7 +869,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId);
userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask);
return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx);
return new AuthenticatedClientSessionAdapter(session,this, entity, clientSession.getClient(), sessionToImportInto, userSessionUpdateTx, clientSessionUpdateTx, offline);
}
private static class RegisterClientSessionTask implements SessionUpdateTask<UserSessionEntity> {

View file

@ -212,7 +212,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionsCache = ispn.getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
return realm.getSsoSessionIdleTimeout() * 1000;
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
return realm.getSsoSessionMaxLifespan() * 1000;
});
if (sessionsRemoteCache) {
@ -221,7 +222,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
Cache<UUID, SessionEntityWrapper<AuthenticatedClientSessionEntity>> clientSessionsCache = ispn.getCache(InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME);
checkRemoteCache(session, clientSessionsCache, (RealmModel realm) -> {
return realm.getSsoSessionIdleTimeout() * 1000;
// We won't write to the remoteCache during token refresh, so the timeout needs to be longer.
return realm.getSsoSessionMaxLifespan() * 1000;
});
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME);

View file

@ -212,7 +212,7 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
.getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
.shouldSaveUserSessionToRemoteCache(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
}
@Override

View file

@ -17,11 +17,16 @@
package org.keycloak.models.sessions.infinispan.changes.sessions;
import java.util.UUID;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.AuthenticatedClientSessionAdapter;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
@ -41,7 +46,72 @@ public class LastSessionRefreshChecker {
}
public SessionUpdateTask.CrossDCMessageStatus getCrossDCMessageStatus(KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
public SessionUpdateTask.CrossDCMessageStatus shouldSaveUserSessionToRemoteCache(
KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
SessionUpdateTask.CrossDCMessageStatus baseChecks = baseChecks(kcSession, realm ,offline);
if (baseChecks != null) {
return baseChecks;
}
String userSessionId = sessionWrapper.getEntity().getId();
if (offline) {
Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
if (lsrr == null) {
lsrr = sessionWrapper.getEntity().getStarted();
}
if (lsrr + (realm.getOfflineSessionIdleTimeout() / 2) <= newLastSessionRefresh) {
logger.debugf("We are going to write remotely userSession %s. Remote last session refresh: %d, New last session refresh: %d",
userSessionId, lsrr, newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
}
if (logger.isDebugEnabled()) {
logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", userSessionId, newLastSessionRefresh);
}
LastSessionRefreshStore storeToUse = offline ? offlineStore : store;
storeToUse.putLastSessionRefresh(kcSession, userSessionId, realm.getId(), newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
}
public SessionUpdateTask.CrossDCMessageStatus shouldSaveClientSessionToRemoteCache(
KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<AuthenticatedClientSessionEntity> sessionWrapper, UserSessionModel userSession, boolean offline, int newTimestamp) {
SessionUpdateTask.CrossDCMessageStatus baseChecks = baseChecks(kcSession, realm ,offline);
if (baseChecks != null) {
return baseChecks;
}
UUID clientSessionId = sessionWrapper.getEntity().getId();
if (offline) {
Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(AuthenticatedClientSessionEntity.LAST_TIMESTAMP_REMOTE);
if (lsrr == null) {
lsrr = userSession.getStarted();
}
if (lsrr + (realm.getOfflineSessionIdleTimeout() / 2) <= newTimestamp) {
logger.debugf("We are going to write remotely for clientSession %s. Remote timestamp: %d, New timestamp: %d",
clientSessionId, lsrr, newTimestamp);
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
}
if (logger.isDebugEnabled()) {
logger.debugf("Skip writing timestamp to the remoteCache. ClientSession %s timestamp %d", clientSessionId, newTimestamp);
}
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
}
private SessionUpdateTask.CrossDCMessageStatus baseChecks(KeycloakSession kcSession, RealmModel realm, boolean offline) {
// revokeRefreshToken always writes everything to remoteCache immediately
if (realm.isRevokeRefreshToken()) {
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
@ -53,29 +123,13 @@ public class LastSessionRefreshChecker {
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
// Received the message from the other DC that we should update the lastSessionRefresh in local cluster
Boolean ignoreRemoteCacheUpdate = (Boolean) kcSession.getAttribute(LastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE);
if (ignoreRemoteCacheUpdate != null && ignoreRemoteCacheUpdate) {
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
}
Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
if (lsrr == null) {
logger.debugf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
int idleTimeout = offline ? realm.getOfflineSessionIdleTimeout() : realm.getSsoSessionIdleTimeout();
if (lsrr + (idleTimeout / 2) <= newLastSessionRefresh) {
logger.debugf("We are going to write remotely. Remote last session refresh: %d, New last session refresh: %d", (int) lsrr, newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", sessionWrapper.getEntity().getId(), newLastSessionRefresh);
storeToUse.putLastSessionRefresh(kcSession, sessionWrapper.getEntity().getId(), realm.getId(), newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
return null;
}
}

View file

@ -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()) {

View file

@ -23,6 +23,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.timer.TimerProvider;
/**
@ -30,15 +31,19 @@ import org.keycloak.timer.TimerProvider;
*/
public class LastSessionRefreshStoreFactory {
// Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes
// Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes should be sent
public static final long DEFAULT_TIMER_INTERVAL_MS = 5000;
// Max interval between messages. It means that when message is sent to second DC, then another message will be sent at least after 60 seconds.
public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = 60;
public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = SessionTimeoutHelper.PERIODIC_TASK_INTERVAL_SECONDS;
// Max count of lastSessionRefreshes. It count of lastSessionRefreshes reach this value, the message is sent to second DC
// Max count of lastSessionRefreshes. If count of lastSessionRefreshes reach this value, the message is sent to second DC
public static final int DEFAULT_MAX_COUNT = 100;
// Name of periodic tasks to send events to the other DCs
public static final String LSR_PERIODIC_TASK_NAME = "lastSessionRefreshes";
public static final String LSR_OFFLINE_PERIODIC_TASK_NAME = "lastSessionRefreshes-offline";
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, boolean offline) {
return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline);
@ -46,7 +51,7 @@ public class LastSessionRefreshStoreFactory {
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) {
String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes";
String eventKey = offline ? LSR_OFFLINE_PERIODIC_TASK_NAME : LSR_PERIODIC_TASK_NAME;
LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
// Register listener

View file

@ -27,6 +27,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.jboss.logging.Logger;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
import java.util.UUID;
@ -37,6 +39,11 @@ import java.util.UUID;
@SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class)
public class AuthenticatedClientSessionEntity extends SessionEntity {
public static final Logger logger = Logger.getLogger(AuthenticatedClientSessionEntity.class);
// Metadata attribute, which contains the last timestamp available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
public static final String LAST_TIMESTAMP_REMOTE = "lstr";
private String authMethod;
private String redirectUri;
private volatile int timestamp;
@ -157,6 +164,31 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
return id != null ? id.hashCode() : 0;
}
@Override
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
int timestampRemote = getTimestamp();
SessionEntityWrapper entityWrapper;
if (localEntityWrapper == null) {
entityWrapper = new SessionEntityWrapper<>(this);
} else {
AuthenticatedClientSessionEntity localClientSession = (AuthenticatedClientSessionEntity) localEntityWrapper.getEntity();
// local timestamp should always contain the bigger
if (timestampRemote < localClientSession.getTimestamp()) {
setTimestamp(localClientSession.getTimestamp());
}
entityWrapper = new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
}
entityWrapper.putLocalMetadataNoteInt(LAST_TIMESTAMP_REMOTE, timestampRemote);
logger.debugf("Updating client session entity %s. timestamp=%d, timestampRemote=%d", getId(), getTimestamp(), timestampRemote);
return entityWrapper;
}
public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
@Override

View file

@ -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;
}

View file

@ -19,7 +19,6 @@ package org.keycloak.models.sessions.infinispan.remotestore;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.keycloak.common.util.Retry;
import org.keycloak.common.util.Time;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -34,9 +33,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -78,8 +75,8 @@ public class RemoteCacheInvoker {
long loadedMaxIdleTimeMs = context.maxIdleTimeLoader.getMaxIdleTimeMs(realm);
// Double the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
final long maxIdleTimeMs = loadedMaxIdleTimeMs * 2;
// Increase the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
final long maxIdleTimeMs = loadedMaxIdleTimeMs + 1800000;
if (logger.isTraceEnabled()) {
logger.tracef("Running task '%s' on remote cache '%s' . Key is '%s'", operation, cacheName, key);
@ -115,7 +112,6 @@ public class RemoteCacheInvoker {
remoteCache.put(key, sessionWrapper.forTransport(), task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
break;
case ADD_IF_ABSENT:
final int currentTime = Time.currentTime();
SessionEntityWrapper<V> existing = remoteCache
.withFlags(Flag.FORCE_RETURN_VALUE)
.putIfAbsent(key, sessionWrapper.forTransport(), -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
@ -123,8 +119,6 @@ public class RemoteCacheInvoker {
logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
} else {
sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
}
break;
case REPLACE:

View file

@ -76,6 +76,7 @@ public class ConcurrencyJDGSessionsCacheTest {
private static final UUID CLIENT_1_UUID = UUID.randomUUID();
public static void main(String[] args) throws Exception {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME);
@ -187,7 +188,6 @@ public class ConcurrencyJDGSessionsCacheTest {
", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get());
System.out.println("remoteCache1.notes: " + ((UserSessionEntity) remoteCache1.get("123")).getNotes().size() );
System.out.println("remoteCache2.notes: " + ((UserSessionEntity) remoteCache2.get("123")).getNotes().size() );

View file

@ -156,12 +156,13 @@ public class InfinispanKeyStorageProviderTest {
protected Cache<String, PublicKeysEntry> getKeysCache() {
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
gcb.globalJmxStatistics().allowDuplicateDomains(true);
gcb.globalJmxStatistics().allowDuplicateDomains(true).enabled(true);
final DefaultCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder cb = new ConfigurationBuilder();
cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX);
cb.jmxStatistics().enabled(true);
Configuration cfg = cb.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, cfg);

View file

@ -0,0 +1,55 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.utils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SessionTimeoutHelper {
/**
* Interval specifies maximum time, for which the "userSession.lastSessionRefresh" may contain stale value.
*
* For example, if there are 2 datacenters and sessionRefresh will happen on DC1, then the message about the updated lastSessionRefresh may
* be sent to the DC2 later (EG. Some periodic thread will send the updated lastSessionRefresh times in batches with 60 seconds delay).
*/
public static final int PERIODIC_TASK_INTERVAL_SECONDS = 60;
/**
* The maximum time difference, which will be still tolerated when checking userSession idle timeout.
*
* For example, if there are 2 datacenters and sessionRefresh happened on DC1, then we still want to tolerate some timeout on DC2 due the
* fact that lastSessionRefresh of current userSession may be updated later from DC1.
*
* See {@link #PERIODIC_TASK_INTERVAL_SECONDS}
*/
public static final int IDLE_TIMEOUT_WINDOW_SECONDS = 120;
/**
* The maximum time difference, which will be still tolerated when checking userSession idle timeout with periodic cleaner threads.
*
* Just the sessions, with the timeout bigger than this value are considered really time-outed and can be garbage-collected (Considering the cross-dc
* environment and the fact that some session updates on different DC can be postponed and seen on current DC with some delay).
*
* See {@link #PERIODIC_TASK_INTERVAL_SECONDS} and {@link #IDLE_TIMEOUT_WINDOW_SECONDS}
*/
public static final int PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS = 180;
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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();

View file

@ -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();

View file

@ -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

View file

@ -35,7 +35,7 @@ public class BasicTimerProviderFactory implements TimerProviderFactory {
private Timer timer;
private ConcurrentMap<String, TimerTask> scheduledTasks = new ConcurrentHashMap<String, TimerTask>();
private ConcurrentMap<String, TimerTaskContextImpl> scheduledTasks = new ConcurrentHashMap<>();
@Override
public TimerProvider create(KeycloakSession session) {
@ -63,11 +63,11 @@ public class BasicTimerProviderFactory implements TimerProviderFactory {
return "basic";
}
protected TimerTask putTask(String taskName, TimerTask task) {
protected TimerTaskContextImpl putTask(String taskName, TimerTaskContextImpl task) {
return scheduledTasks.put(taskName, task);
}
protected TimerTask removeTask(String taskName) {
protected TimerTaskContextImpl removeTask(String taskName) {
return scheduledTasks.remove(taskName);
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.timer.basic;
import java.util.TimerTask;
import org.keycloak.timer.TimerProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TimerTaskContextImpl implements TimerProvider.TimerTaskContext {
private final Runnable runnable;
final TimerTask timerTask;
private final long intervalMillis;
public TimerTaskContextImpl(Runnable runnable, TimerTask timerTask, long intervalMillis) {
this.runnable = runnable;
this.timerTask = timerTask;
this.intervalMillis = intervalMillis;
}
@Override
public Runnable getRunnable() {
return runnable;
}
@Override
public long getIntervalMillis() {
return intervalMillis;
}
}

View file

@ -40,6 +40,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.AdminEventRepresentation;
@ -49,6 +50,7 @@ import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.components.TestProvider;
import org.keycloak.testsuite.components.TestProviderFactory;
@ -63,6 +65,7 @@ import org.keycloak.testsuite.runonserver.ModuleUtil;
import org.keycloak.testsuite.runonserver.FetchOnServer;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.runonserver.SerializationUtil;
import org.keycloak.timer.TimerProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.MediaType;
@ -83,21 +86,24 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TestingResourceProvider implements RealmResourceProvider {
private KeycloakSession session;
private final KeycloakSession session;
private final Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks;
@Override
public Object getResource() {
return this;
}
public TestingResourceProvider(KeycloakSession session) {
public TestingResourceProvider(KeycloakSession session, Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks) {
this.session = session;
this.suspendedTimerTasks = suspendedTimerTasks;
}
@POST
@ -134,9 +140,9 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
@GET
@Path("/get-user-session")
@Path("/get-last-session-refresh")
@Produces(MediaType.APPLICATION_JSON)
public Integer getLastSessionRefresh(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId) {
public Integer getLastSessionRefresh(@QueryParam("realm") final String name, @QueryParam("session") final String sessionId, @QueryParam("offline") boolean offline) {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(name);
@ -144,7 +150,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
throw new NotFoundException("Realm not found");
}
UserSessionModel sessionModel = session.sessions().getUserSession(realm, sessionId);
UserSessionModel sessionModel = offline ? session.sessions().getOfflineUserSession(realm, sessionId) : session.sessions().getUserSession(realm, sessionId);
if (sessionModel == null) {
throw new NotFoundException("Session not found");
}
@ -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<String, TimerProvider.TimerTaskContext> task : suspendedTimerTasks.entrySet()) {
timer.schedule(task.getValue().getRunnable(), task.getValue().getIntervalMillis(), task.getKey());
}
suspendedTimerTasks.clear();
return Response.noContent().build();
}
@POST
@Path("/run-on-server")
@Consumes(MediaType.TEXT_PLAIN_UTF_8)

View file

@ -17,20 +17,26 @@
package org.keycloak.testsuite.rest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory;
import org.keycloak.timer.TimerProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TestingResourceProviderFactory implements RealmResourceProviderFactory {
private Map<String, TimerProvider.TimerTaskContext> suspendedTimerTasks = new ConcurrentHashMap<>();
@Override
public RealmResourceProvider create(KeycloakSession session) {
return new TestingResourceProvider(session);
return new TestingResourceProvider(session, suspendedTimerTasks);
}
@Override

View file

@ -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)

View file

@ -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);

View file

@ -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();

View file

@ -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);
}
}

View file

@ -59,6 +59,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
private int sessions01;
private int sessions02;
private int clientSessions01;
private int clientSessions02;
private int remoteSessions01;
private int remoteSessions02;
@ -102,12 +104,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void testRealmRemoveSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME) InfinispanStatistics clientCacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,false, cacheDc1Statistics, cacheDc2Statistics, true);
// log.infof("Sleeping!");
// Thread.sleep(10000000);
@ -118,13 +120,13 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getAdminClient().realm(REALM_NAME).remove();
// Assert sessions removed on node1 and node2 and on remote caches
assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
// Return last used accessTokenResponse
private List<OAuthClient.AccessTokenResponse> createInitialSessions(String cacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception {
private List<OAuthClient.AccessTokenResponse> createInitialSessions(String cacheName, String clientSessionsCacheName, boolean offline, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, boolean includeRemoteStats) throws Exception {
// Enable second DC
enableDcOnLoadBalancer(DC.SECOND);
@ -132,9 +134,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
// Check sessions count before test
sessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
sessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
clientSessions01 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
clientSessions02 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
remoteSessions01 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
remoteSessions02 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d", sessions01, sessions02, remoteSessions01, remoteSessions02);
log.infof("Before creating sessions: sessions01: %d, sessions02: %d, remoteSessions01: %d, remoteSessions02: %d, clientSessions01: %d, clientSessions02: %d",
sessions01, sessions02, remoteSessions01, remoteSessions02, clientSessions01, clientSessions02);
// Create 20 user sessions
oauth.realm(REALM_NAME);
@ -152,9 +157,12 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
Retry.execute(() -> {
int sessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
int sessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
int clientSessions11 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
int clientSessions12 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
int remoteSessions11 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
int remoteSessions12 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d", sessions11, sessions12, remoteSessions11, remoteSessions12);
log.infof("After creating sessions: sessions11: %d, sessions12: %d, remoteSessions11: %d, remoteSessions12: %d, clientSessions11: %d, clientSessions12: %d",
sessions11, sessions12, remoteSessions11, remoteSessions12, clientSessions11, clientSessions12);
Assert.assertEquals(sessions11, sessions01 + SESSIONS_COUNT);
Assert.assertEquals(sessions12, sessions02 + SESSIONS_COUNT);
@ -169,11 +177,14 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
}
private void assertStatisticsExpected(String messagePrefix, String cacheName, InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc,
int sessions1Expected, int sessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, boolean checkSomeMessagesSentBetweenDCs) {
private void assertStatisticsExpected(String messagePrefix, String cacheName, String clientSessionsCacheName,
InfinispanStatistics cacheDc1Statistics, InfinispanStatistics cacheDc2Statistics, InfinispanStatistics channelStatisticsCrossDc,
int sessions1Expected, int sessions2Expected, int clientSessions1Expected, int clientSessions2Expected, int remoteSessions1Expected, int remoteSessions2Expected, boolean checkSomeMessagesSentBetweenDCs) {
Retry.execute(() -> {
int sessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(cacheName).size();
int sessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(cacheName).size();
int clientSessions1 = getTestingClientForStartedNodeInDc(0).testing().cache(clientSessionsCacheName).size();
int clientSessions2 = getTestingClientForStartedNodeInDc(1).testing().cache(clientSessionsCacheName).size();
int remoteSessions1 = (Integer) cacheDc1Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
int remoteSessions2 = (Integer) cacheDc2Statistics.getSingleStatistics(InfinispanStatistics.Constants.STAT_CACHE_NUMBER_OF_ENTRIES);
long messagesCount = (Long) channelStatisticsCrossDc.getSingleStatistics(InfinispanStatistics.Constants.STAT_CHANNEL_SENT_MESSAGES);
@ -181,6 +192,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
Assert.assertEquals(sessions1, sessions1Expected);
Assert.assertEquals(sessions2, sessions2Expected);
Assert.assertEquals(clientSessions1, clientSessions1Expected);
Assert.assertEquals(clientSessions2, clientSessions2Expected);
Assert.assertEquals(remoteSessions1, remoteSessions1Expected);
Assert.assertEquals(remoteSessions2, remoteSessions2Expected);
@ -195,11 +208,11 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void testRealmRemoveOfflineSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,true, cacheDc1Statistics, cacheDc2Statistics, true);
channelStatisticsCrossDc.reset();
@ -207,18 +220,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getAdminClient().realm(REALM_NAME).remove();
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After realm remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testLogoutAllInRealm(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
channelStatisticsCrossDc.reset();
@ -226,18 +239,18 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getAdminClient().realm(REALM_NAME).logoutAll();
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After realm logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testPeriodicExpiration(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
public void testPeriodicExpirationSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,false, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
// Assert I am able to refresh
OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
@ -250,8 +263,9 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
// Nothing yet expired. It may happen that no message sent between DCs
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, clientSessions01 + SESSIONS_COUNT, clientSessions02 + SESSIONS_COUNT,
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
// Set time offset
@ -269,8 +283,55 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testPeriodicExpirationOfflineSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
OAuthClient.AccessTokenResponse lastAccessTokenResponse = createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
true, cacheDc1Statistics, cacheDc2Statistics, true).get(SESSIONS_COUNT - 1);
// Assert I am able to refresh
OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
Assert.assertNotNull(refreshResponse.getRefreshToken());
Assert.assertNull(refreshResponse.getError());
channelStatisticsCrossDc.reset();
// Remove expired in DC0
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
// Nothing yet expired. It may happen that no message sent between DCs
assertStatisticsExpected("After remove expired - 1", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01 + SESSIONS_COUNT, sessions02 + SESSIONS_COUNT, clientSessions01 + SESSIONS_COUNT, clientSessions02 + SESSIONS_COUNT,
remoteSessions01 + SESSIONS_COUNT, remoteSessions02 + SESSIONS_COUNT, false);
// Set time offset
setTimeOffset(10000000);
// Assert I am not able to refresh anymore
refreshResponse = oauth.doRefreshTokenRequest(lastAccessTokenResponse.getRefreshToken(), "password");
Assert.assertNull(refreshResponse.getRefreshToken());
Assert.assertNotNull(refreshResponse.getError());
channelStatisticsCrossDc.reset();
// Remove expired in DC0
getTestingClientForStartedNodeInDc(0).testing().removeExpired(REALM_NAME);
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After remove expired - 2", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@ -278,10 +339,10 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void testUserRemoveSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,false, cacheDc1Statistics, cacheDc2Statistics, true);
// log.infof("Sleeping!");
// Thread.sleep(10000000);
@ -293,17 +354,17 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After user remove", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testUserRemoveOfflineSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, true, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,true, cacheDc1Statistics, cacheDc2Statistics, true);
// log.infof("Sleeping!");
// Thread.sleep(10000000);
@ -315,18 +376,19 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After user remove", InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME,
cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testLogoutUser(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, true);
channelStatisticsCrossDc.reset();
@ -336,29 +398,33 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
getAdminClient().realm(REALM_NAME).deleteSession(userSession.getId());
// Just one session expired.
assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01 + SESSIONS_COUNT - 1, sessions02 + SESSIONS_COUNT - 1, remoteSessions01 + SESSIONS_COUNT - 1, remoteSessions02 + SESSIONS_COUNT - 1, true);
assertStatisticsExpected("After logout single session", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01 + SESSIONS_COUNT - 1, sessions02 + SESSIONS_COUNT - 1, clientSessions01 + SESSIONS_COUNT - 1, clientSessions02 + SESSIONS_COUNT - 1,
remoteSessions01 + SESSIONS_COUNT - 1, remoteSessions02 + SESSIONS_COUNT - 1, true);
// Logout all sessions for user now
user.logout();
// Assert sessions removed on node1 and node2 and on remote caches.
assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, remoteSessions01, remoteSessions02, true);
assertStatisticsExpected("After user logout", InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
cacheDc1Statistics, cacheDc2Statistics, channelStatisticsCrossDc,
sessions01, sessions02, clientSessions01, clientSessions02, remoteSessions01, remoteSessions02, true);
}
@Test
public void testLogoutUserWithFailover(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.USER_SESSION_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
// Start node2 on first DC
startBackendNode(DC.FIRST, 1);
// Don't include remote stats. Size is smaller because of distributed cache
List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, false, cacheDc1Statistics, cacheDc2Statistics, false);
List<OAuthClient.AccessTokenResponse> responses = createInitialSessions(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME,
false, cacheDc1Statistics, cacheDc2Statistics, false);
// Kill node2 now. Around 10 sessions (half of SESSIONS_COUNT) will be lost on Keycloak side. But not on infinispan side
stopBackendNode(DC.FIRST, 1);
@ -396,8 +462,8 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
@Test
public void testPeriodicExpirationAuthSessions(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, managementPortProperty = "cache.server.management.port", cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc1Statistics,
@JmxInfinispanCacheStatistics(dc=DC.SECOND, managementPortProperty = "cache.server.2.management.port", cacheName=InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME) InfinispanStatistics cacheDc2Statistics,
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
createInitialAuthSessions();

View file

@ -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

View file

@ -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");