From eedc4ceb181df7f89b9133cc8dcdd6378aef892b Mon Sep 17 00:00:00 2001 From: Lex Cao Date: Thu, 21 Sep 2023 02:40:55 +0800 Subject: [PATCH] Fix unexpected expiration when import offline client session Closes #23397 --- .../InfinispanUserSessionProvider.java | 3 + .../UserSessionProviderOfflineModelTest.java | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 72b64e104f4..c04cfcb2224 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -1063,6 +1063,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(clientSession, sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), offline); + // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value + entity.setTimestamp(sessionToImportInto.getLastSessionRefresh()); + if (checkExpiration) { SessionFunction lifespanChecker = offline ? SessionTimeouts::getOfflineClientSessionLifespanMs : SessionTimeouts::getClientSessionLifespanMs; diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java index 18ab595248b..4d4d18ebd21 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderOfflineModelTest.java @@ -49,8 +49,10 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -413,6 +415,61 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest { }); } + @Test + public void testLoadingOfflineClientSessionWhenCreatedBeforeSessionTime() { + // setup idle timeout for the realm + int idleTimeout = (int) TimeUnit.DAYS.toSeconds(1); + withRealm(realmId, (session, realmModel) -> { + realmModel.setClientOfflineSessionIdleTimeout(idleTimeout); + return null; + }); + + // create online user and client sessions + inComittedTransaction((Consumer) session -> UserSessionPersisterProviderTest.createSessions(session, realmId)); + + // create offline user and client sessions + List offlineUserSessionIds = withRealm(realmId, (session, realm) -> session.sessions() + .getUserSessionsStream(realm, realm.getClientByClientId("test-app")) + .map(userSession -> { + UserSessionModel offlineUserSession = Optional.ofNullable( + session.sessions().getOfflineUserSession(realm, userSession.getId()) + ).orElseGet(() -> session.sessions().createOfflineUserSession(userSession)); + + userSession.getAuthenticatedClientSessions() + .values() + .forEach(clientSession -> { + // set timestamp manually to make sure the client session is created before session time + // this simulates the cases when the offline client sessions are created before the session time + clientSession.setTimestamp(Time.currentTime() - idleTimeout * 2); + + session.sessions().createOfflineClientSession(clientSession, offlineUserSession); + }); + + return offlineUserSession.getId(); + } + ).collect(Collectors.toList()) + ); + + withRealm(realmId, (session, realm) -> { + // remove offline client sessions from the cache + // this simulates the cases when offline client sessions are lost from the cache due to various reasons (a cache limit/expiration/preloading issue) + session.getProvider(InfinispanConnectionProvider.class) + .getCache(InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME).clear(); + + String clientUUID = realm.getClientByClientId("test-app").getId(); + + offlineUserSessionIds.forEach(id -> { + UserSessionModel offlineUserSession = session.sessions().getOfflineUserSession(realm, id); + + // each associated offline client session should be found by looking into persister + AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessionByClient(clientUUID); + Assert.assertNotNull(offlineClientSession); + Assert.assertEquals(offlineUserSession.getLastSessionRefresh(), offlineClientSession.getTimestamp()); + }); + return null; + }); + } + private static Set createOfflineSessionIncludeClientSessions(KeycloakSession session, UserSessionModel userSession) { Set offlineSessions = new HashSet<>();