From f7ff7e55d8588927a49e8e46d5a6c791ed497c53 Mon Sep 17 00:00:00 2001 From: Pedro Ruivo Date: Wed, 17 Sep 2025 11:25:51 +0100 Subject: [PATCH] Replace UUID with composite key for client session cache Closes #42547 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Signed-off-by: Alexander Schwartz Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Alexander Schwartz --- .../topics/changes/changes-26_4_0.adoc | 15 ++ .../marshalling/KeycloakModelSchema.java | 2 + .../org/keycloak/marshalling/Marshalling.java | 2 + .../AuthenticatedClientSessionAdapter.java | 29 ++-- .../InfinispanUserSessionProvider.java | 150 ++++++++++-------- .../InfinispanUserSessionProviderFactory.java | 10 +- .../PersistentUserSessionProvider.java | 113 ++++++------- .../infinispan/UserSessionAdapter.java | 74 ++++----- ...onPersistentChangelogBasedTransaction.java | 32 ++-- .../changes/JpaChangesPerformer.java | 26 +-- ...onPersistentChangelogBasedTransaction.java | 5 +- .../AuthenticatedClientSessionEntity.java | 89 ++++------- .../entities/EmbeddedClientSessionKey.java | 37 +++++ .../infinispan/entities/SessionEntity.java | 2 + .../entities/UserSessionEntity.java | 37 ++--- .../remote/RemoteUserSessionProvider.java | 7 +- .../stream/AuthClientSessionSetMapper.java | 2 +- .../stream/UserSessionPredicate.java | 2 +- .../migration/migrators/MigrateTo26_4_0.java | 50 ++++++ ...tentAuthenticatedClientSessionAdapter.java | 8 +- .../datastore/DefaultMigrationManager.java | 2 + .../AuthenticatedClientSessionModel.java | 4 +- .../keycloak/models/UserSessionProvider.java | 30 +++- .../managers/AuthenticationManager.java | 8 +- .../UserSessionPersisterProviderTest.java | 31 ++-- .../session/UserSessionProviderModelTest.java | 34 ++-- .../util/cli/AbstractSessionCacheCommand.java | 2 +- 27 files changed, 429 insertions(+), 374 deletions(-) create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/EmbeddedClientSessionKey.java create mode 100644 model/storage-private/src/main/java/org/keycloak/migration/migrators/MigrateTo26_4_0.java diff --git a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc index 54d625cd664..531d91b7993 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc @@ -111,6 +111,11 @@ configured directly by {project_name}: Configuring ports using the old properties has not changed, but using the CLI options is recommended because the previous method could be deprecated. +=== Internal representation of client sessions changed + +The cache key of the authenticated client sessions has changed for embedded Infinispan, while the public APIs have not changed. +Due to this, you should not run 26.4.x concurrently in a cluster with previous versions. + === External IDP tokens automatically refreshed When using the `+/realms/{realm-name}/broker/{provider_alias}/token+` endpoint for an OAuth 2.0 IDP that provides refresh tokens and JSON responses or for OIDC IDPs, the tokens will be automatically refreshed each time they are retrieved via the endpoint if the access token has expired and the IDP provided a refresh token. @@ -256,6 +261,16 @@ Configuration of the default cache configurations in `conf/cache-ispn.xml`, or i In a future major release, the start-up will fail if default cache configurations are stated in those files and the option is not specified. +=== Simplified API for UserSessionProvider + +In order to retrieve a client session via `UserSessionProvider#getClientSession`, you no longer need to pass in the client session ID. +The old methods have been deprecated and will be removed in a future release. +You should also review the other methods that are deprecated for removal in this class. + +=== Simplified API for AuthenticatedClientSessionModel + +The `clientId` note in the authenticated client session is an internal note present only when using the embedded caches, and is now deprecated for removal. Instead, use the `getClient()` method. + // ------------------------ Removed features ------------------------ // == Removed features diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java index b9b6b75d0ed..ca105d07754 100644 --- a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakModelSchema.java @@ -85,6 +85,7 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.entities.ClientSessionKey; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.RemoteAuthenticatedClientSessionEntity; @@ -206,6 +207,7 @@ import org.keycloak.storage.managers.UserStorageSyncManager; AuthenticatedClientSessionEntity.class, AuthenticationSessionEntity.class, ClientSessionKey.class, + EmbeddedClientSessionKey.class, LoginFailureEntity.class, LoginFailureKey.class, RemoteAuthenticatedClientSessionEntity.class, diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java index e6b3be0b9a1..8cbcdd53862 100644 --- a/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/Marshalling.java @@ -182,6 +182,8 @@ public final class Marshalling { public static final int RELOAD_CERTIFICATE_FUNCTION = 65615; + public static final int EMBEDDED_CLIENT_SESSION_KEY = 65616; + public static void configure(GlobalConfigurationBuilder builder) { getSchemas().forEach(builder.serialization()::addContextInitializer); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java index ecacfe67017..063e947d8cc 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -17,9 +17,8 @@ package org.keycloak.models.sessions.infinispan; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.keycloak.common.util.Time; import org.keycloak.models.AuthenticatedClientSessionModel; @@ -33,8 +32,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.SessionsChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; - -import java.util.UUID; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; /** * @author Marek Posolda @@ -42,15 +40,18 @@ import java.util.UUID; public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel { private final KeycloakSession kcSession; - private AuthenticatedClientSessionEntity entity; + private final AuthenticatedClientSessionEntity entity; private final ClientModel client; - private final SessionsChangelogBasedTransaction clientSessionUpdateTx; + private final SessionsChangelogBasedTransaction clientSessionUpdateTx; private UserSessionModel userSession; - private boolean offline; + private final boolean offline; + private final EmbeddedClientSessionKey cacheKey; public AuthenticatedClientSessionAdapter(KeycloakSession kcSession, AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionModel userSession, - SessionsChangelogBasedTransaction clientSessionUpdateTx, boolean offline) { + SessionsChangelogBasedTransaction clientSessionUpdateTx, + EmbeddedClientSessionKey cacheKey, + boolean offline) { if (userSession == null) { throw new NullPointerException("userSession must not be null"); } @@ -61,10 +62,11 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes this.client = client; this.clientSessionUpdateTx = clientSessionUpdateTx; this.offline = offline; + this.cacheKey = cacheKey; } private void update(ClientSessionUpdateTask task) { - clientSessionUpdateTx.addTask(entity.getId(), task); + clientSessionUpdateTx.addTask(cacheKey, task); } /** @@ -84,7 +86,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes SessionUpdateTask removeTask = Tasks.removeSync(offline); - clientSessionUpdateTx.addTask(entity.getId(), removeTask); + clientSessionUpdateTx.addTask(cacheKey, removeTask); } @Override @@ -117,7 +119,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public String getId() { - return entity.getId().toString(); + return cacheKey.toId(); } @Override @@ -258,10 +260,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes @Override public Map getNotes() { - if (entity.getNotes().isEmpty()) return Collections.emptyMap(); - Map copy = new HashMap<>(); - copy.putAll(entity.getNotes()); - return copy; + return new ConcurrentHashMap<>(entity.getNotes()); } @Override 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 b3645021091..9cb2035022e 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 @@ -23,7 +23,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; -import java.util.UUID; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -35,6 +35,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.infinispan.Cache; +import org.infinispan.commons.api.AsyncCache; import org.infinispan.commons.util.concurrent.CompletionStages; import org.infinispan.stream.CacheCollectors; import org.jboss.logging.Logger; @@ -61,7 +62,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask; import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; @@ -77,6 +78,9 @@ import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator; import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; import org.keycloak.utils.StreamsUtil; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME; import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER; import static org.keycloak.utils.StreamsUtil.paginatedStream; @@ -91,8 +95,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi protected final InfinispanChangelogBasedTransaction sessionTx; protected final InfinispanChangelogBasedTransaction offlineSessionTx; - protected final InfinispanChangelogBasedTransaction clientSessionTx; - protected final InfinispanChangelogBasedTransaction offlineClientSessionTx; + protected final InfinispanChangelogBasedTransaction clientSessionTx; + protected final InfinispanChangelogBasedTransaction offlineClientSessionTx; protected final SessionEventsSenderTransaction clusterEventsSenderTx; @@ -109,8 +113,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi InfinispanKeyGenerator keyGenerator, InfinispanChangelogBasedTransaction sessionTx, InfinispanChangelogBasedTransaction offlineSessionTx, - InfinispanChangelogBasedTransaction clientSessionTx, - InfinispanChangelogBasedTransaction offlineClientSessionTx, + InfinispanChangelogBasedTransaction clientSessionTx, + InfinispanChangelogBasedTransaction offlineClientSessionTx, SessionFunction offlineSessionCacheEntryLifespanAdjuster, SessionFunction offlineClientSessionCacheEntryLifespanAdjuster) { this.session = session; @@ -138,11 +142,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi return offline ? offlineSessionTx : sessionTx; } - protected Cache> getClientSessionCache(boolean offline) { + protected Cache> getClientSessionCache(boolean offline) { return offline ? offlineClientSessionTx.getCache() : clientSessionTx.getCache(); } - protected InfinispanChangelogBasedTransaction getClientSessionTransaction(boolean offline) { + protected InfinispanChangelogBasedTransaction getClientSessionTransaction(boolean offline) { return offline ? offlineClientSessionTx : clientSessionTx; } @@ -158,20 +162,20 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { - InfinispanChangelogBasedTransaction clientSessionUpdateTx = clientSessionTx; - final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionUpdateTx.getCache()); - var entity = AuthenticatedClientSessionEntity.create(clientSessionId, realm, client, userSession); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = clientSessionTx; + final EmbeddedClientSessionKey key = new EmbeddedClientSessionKey(userSession.getId(), client.getId()); + var entity = AuthenticatedClientSessionEntity.create(realm, client, userSession); - AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, false); + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, key, false); // For now, the clientSession is considered transient in case that userSession was transient UserSessionModel.SessionPersistenceState persistenceState = userSession.getPersistenceState() != null ? userSession.getPersistenceState() : UserSessionModel.SessionPersistenceState.PERSISTENT; SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); - clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState); + clientSessionUpdateTx.addTask(key, createClientSessionTask, entity, persistenceState); - sessionTx.addTask(userSession.getId(), new RegisterClientSessionTask(client.getId(), clientSessionId)); + sessionTx.addTask(userSession.getId(), new RegisterClientSessionTask(key.clientId())); return adapter; } @@ -208,6 +212,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi if ("26.0.0".equals(modelVersion)) { log.debug("Clear caches to migrate to Infinispan Protostream"); CompletionStages.join(session.getProvider(InfinispanConnectionProvider.class).migrateToProtoStream()); + } else if ("26.4.0".equals(modelVersion)) { + log.debug("Clear caches as client session entries are now outdated and are not migrated"); + // This is a best-effort approach: Even if due to a rolling update some entries are left there, the checking of sessions and tokens does not depend on them. + // Refreshing of tokens will still work even if the user session does not contain the list of client sessions. + // This still keeps the user session cache to keep users logged in on a best effort basis. + // Only the offline user sessions cache is cleared, but not the regular user sessions cache is cleared. + // All client session caches regular and offline client sessions are cleared as usual. + var stage = CompletionStages.aggregateCompletionStage(); + var provider = session.getProvider(InfinispanConnectionProvider.class); + Stream.of(OFFLINE_USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME) + .map(s -> provider.getCache(s, false)) + .filter(Objects::nonNull) + .map(AsyncCache::clearAsync) + .forEach(stage::dependsOn); + CompletionStages.join(stage.freeze()); } } @@ -297,25 +316,26 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi return userSessionEntityToImport; } - private Map> computeClientSessionsToImport(UserSessionModel persistentUserSession, UserSessionEntity userSessionToImport) { - Map> clientSessionsById = new HashMap<>(); - AuthenticatedClientSessionStore clientSessions = userSessionToImport.getAuthenticatedClientSessions(); + private Map> computeClientSessionsToImport(UserSessionModel persistentUserSession, UserSessionEntity userSessionToImport) { + Map> clientSessionsById = new HashMap<>(); + Set clientSessions = userSessionToImport.getClientSessions(); + String userSessionId = userSessionToImport.getId(); int lastSessionRefresh = userSessionToImport.getLastSessionRefresh(); String realmId = userSessionToImport.getRealmId(); for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { String clientUUID = entry.getKey(); AuthenticatedClientSessionModel clientSession = entry.getValue(); AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(clientSession, - realmId, clientUUID, true); + realmId, clientUUID); // Update timestamp to the same value as userSession. // LastSessionRefresh of userSession from DB will have the correct value. clientSessionToImport.setTimestamp(lastSessionRefresh); - clientSessionsById.put(clientSessionToImport.getId(), new SessionEntityWrapper<>(clientSessionToImport)); + clientSessionsById.put(new EmbeddedClientSessionKey(userSessionId, clientUUID), new SessionEntityWrapper<>(clientSessionToImport)); // Update userSession entity with the clientSession - clientSessions.put(clientUUID, clientSessionToImport.getId()); + clientSessions.add(clientUUID); } return clientSessionsById; } @@ -372,19 +392,16 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi } @Override - public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, String clientSessionId, boolean offline) { - if (clientSessionId == null) { - return null; - } - - AuthenticatedClientSessionEntity clientSessionEntityFromCache = getClientSessionEntity(UUID.fromString(clientSessionId), offline); + public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, boolean offline) { + var key = new EmbeddedClientSessionKey(userSession.getId(), client.getId()); + AuthenticatedClientSessionEntity clientSessionEntityFromCache = getClientSessionEntity(key, offline); if (clientSessionEntityFromCache != null) { - return wrap(userSession, client, clientSessionEntityFromCache, offline); + return wrap(userSession, client, clientSessionEntityFromCache, key, offline); } // offline client session lookup in the persister if (offline) { - log.debugf("Offline client session is not found in cache, try to load from db, userSession [%s] clientSessionId [%s] clientId [%s]", userSession.getId(), clientSessionId, client.getClientId()); + log.debugf("Offline client session is not found in cache, try to load from db, %s", key); return getClientSessionEntityFromPersistenceProvider(userSession, client); } @@ -403,9 +420,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi getClientSessionTransaction(true), true); } - private AuthenticatedClientSessionEntity getClientSessionEntity(UUID id, boolean offline) { - InfinispanChangelogBasedTransaction tx = getClientSessionTransaction(offline); - SessionEntityWrapper entityWrapper = tx.get(id); + private AuthenticatedClientSessionEntity getClientSessionEntity(EmbeddedClientSessionKey key, boolean offline) { + var tx = getClientSessionTransaction(offline); + SessionEntityWrapper entityWrapper = tx.get(key); return entityWrapper == null ? null : entityWrapper.getEntity(); } @@ -560,7 +577,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi FuturesHelper futures = new FuturesHelper(); Cache> localCache = CacheDecorators.localCache(getCache(offline)); - Cache> localClientSessionCache = CacheDecorators.localCache(getClientSessionCache(offline)); + var localClientSessionCache = CacheDecorators.localCache(getClientSessionCache(offline)); final AtomicInteger userSessionsSize = new AtomicInteger(); @@ -574,8 +591,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi // Remove session from remoteCache too. Use removeAsync for better perf Future future = localCache.removeAsync(userSessionEntity.getKey()); futures.addTask(future); - userSessionEntity.getValue().getEntity().getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { - Future f = localClientSessionCache.removeAsync(clientSessionId); + userSessionEntity.getValue().getEntity().getClientSessions().forEach(clientUUID -> { + Future f = localClientSessionCache.removeAsync(new EmbeddedClientSessionKey(userSessionEntity.getKey(), clientUUID)); futures.addTask(f); }); }); @@ -634,16 +651,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi } protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) { - InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); - InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); - sessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync())); - SessionUpdateTask removeTask = Tasks.removeSync(); - userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); + var clientSessionUpdateTx = getClientSessionTransaction(offline); + sessionEntity.getClientSessions().forEach(clientUUID -> clientSessionUpdateTx.addTask(new EmbeddedClientSessionKey(sessionEntity.getId(), clientUUID), Tasks.removeSync())); + getTransaction(offline).addTask(sessionEntity.getId(), Tasks.removeSync()); } UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline, UserModel user) { InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); - InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + var clientSessionUpdateTx = getClientSessionTransaction(offline); if (entity == null) { return null; @@ -678,9 +693,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi return wrap(realm, entity, offline, user); } - AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, boolean offline) { - InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); - return entity != null ? new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, offline) : null; + AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel client, AuthenticatedClientSessionEntity entity, EmbeddedClientSessionKey key, boolean offline) { + InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); + return entity != null ? new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, key, offline) : null; } UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) { @@ -734,14 +749,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); - InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(true); + var clientSessionUpdateTx = getClientSessionTransaction(true); AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, false); assert offlineClientSession != null; // no expiration checked, it is never null // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); - offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp())); - offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted())); + offlineClientSession.setNote(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp())); + offlineClientSession.setNote(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted())); session.getProvider(UserSessionPersisterProvider.class).createClientSession(clientSession, true); @@ -777,27 +792,28 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi return; } - Map> clientSessionsById = new HashMap<>(); + Map> clientSessionsById = new HashMap<>(); Map> sessionsById = persistentUserSessions.stream() .map((UserSessionModel persistentUserSession) -> { UserSessionEntity userSessionEntityToImport = UserSessionEntity.createFromModel(persistentUserSession); + Set clientSessions = userSessionEntityToImport.getClientSessions(); + String userSessionId = userSessionEntityToImport.getId(); for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { String clientUUID = entry.getKey(); AuthenticatedClientSessionModel clientSession = entry.getValue(); AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(clientSession, - userSessionEntityToImport.getRealmId(), clientUUID, offline); + userSessionEntityToImport.getRealmId(), clientUUID); // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value clientSessionToImport.setTimestamp(userSessionEntityToImport.getLastSessionRefresh()); - clientSessionsById.put(clientSessionToImport.getId(), new SessionEntityWrapper<>(clientSessionToImport)); + clientSessionsById.put(new EmbeddedClientSessionKey(userSessionId, clientUUID), new SessionEntityWrapper<>(clientSessionToImport)); // Update userSession entity with the clientSession - AuthenticatedClientSessionStore clientSessions = userSessionEntityToImport.getAuthenticatedClientSessions(); - clientSessions.put(clientUUID, clientSessionToImport.getId()); + clientSessions.add(clientUUID); } return userSessionEntityToImport; @@ -818,7 +834,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi } // Import client sessions - Cache> clientSessCache = getClientSessionCache(offline); + var clientSessCache = getClientSessionCache(offline); if (importWithExpiration) { importSessionsWithExpiration(clientSessionsById, clientSessCache, @@ -862,10 +878,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession, InfinispanChangelogBasedTransaction userSessionUpdateTx, - InfinispanChangelogBasedTransaction clientSessionUpdateTx, + InfinispanChangelogBasedTransaction clientSessionUpdateTx, boolean checkExpiration) { AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(clientSession, - sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), true); + sessionToImportInto.getRealm().getId(), clientSession.getClient().getId()); // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value entity.setTimestamp(sessionToImportInto.getLastSessionRefresh()); @@ -877,26 +893,27 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi } } - final UUID clientSessionId = entity.getId(); + String clientUUID = clientSession.getClient().getId(); + String userSessionId = sessionToImportInto.getId(); - SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); - clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); - AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); - clientSessions.put(clientSession.getClient().getId(), clientSessionId); + var key = new EmbeddedClientSessionKey(userSessionId, clientUUID); + clientSessionUpdateTx.addTask(key, Tasks.addIfAbsentSync(), entity, UserSessionModel.SessionPersistenceState.PERSISTENT); - userSessionUpdateTx.addTask(sessionToImportInto.getId(), new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId)); + sessionToImportInto.getEntity().getClientSessions().add(clientUUID); - return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, true); + userSessionUpdateTx.addTask(sessionToImportInto.getId(), new RegisterClientSessionTask(clientUUID)); + + return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, key, true); } private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(AuthenticatedClientSessionModel clientSession, - String realmId, String clientId, boolean offline) { - final UUID clientSessionId = keyGenerator.generateKeyUUID(session, getClientSessionCache(offline)); - AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + String realmId, String clientId) { + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); entity.setRealmId(realmId); entity.setClientId(clientId); + entity.setUserSessionId(clientSession.getUserSession().getId()); entity.setAction(clientSession.getAction()); entity.setAuthMethod(clientSession.getProtocol()); @@ -908,13 +925,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi return entity; } - private record RegisterClientSessionTask(String clientUuid, UUID clientSessionId) + private record RegisterClientSessionTask(String clientUuid) implements SessionUpdateTask { @Override public void runUpdate(UserSessionEntity session) { - AuthenticatedClientSessionStore clientSessions = session.getAuthenticatedClientSessions(); - clientSessions.put(clientUuid, clientSessionId); + session.getClientSessions().add(clientUuid); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index b48d61467f7..db696d4d55b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; @@ -50,6 +49,7 @@ import org.keycloak.models.sessions.infinispan.changes.UserSessionPersistentChan import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore; import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; @@ -89,8 +89,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider private CacheHolder sessionCacheHolder; private CacheHolder offlineSessionCacheHolder; - private CacheHolder clientSessionCacheHolder; - private CacheHolder offlineClientSessionCacheHolder; + private CacheHolder clientSessionCacheHolder; + private CacheHolder offlineClientSessionCacheHolder; private long offlineSessionCacheEntryLifespanOverride; @@ -410,8 +410,8 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider private record VolatileTransactions(InfinispanChangelogBasedTransaction sessionTx, InfinispanChangelogBasedTransaction offlineSessionTx, - InfinispanChangelogBasedTransaction clientSessionTx, - InfinispanChangelogBasedTransaction offlineClientSessionTx) {} + InfinispanChangelogBasedTransaction clientSessionTx, + InfinispanChangelogBasedTransaction offlineClientSessionTx) {} private record PersistentTransaction(UserSessionPersistentChangelogBasedTransaction userTx, ClientSessionPersistentChangelogBasedTransaction clientTx) {} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java index a6c6c99e8c3..28356630665 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/PersistentUserSessionProvider.java @@ -17,13 +17,11 @@ package org.keycloak.models.sessions.infinispan; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -37,6 +35,7 @@ import io.reactivex.rxjava3.core.Flowable; import org.infinispan.Cache; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.exceptions.HotRodClientException; +import org.infinispan.commons.api.AsyncCache; import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.util.ByRef; import org.infinispan.commons.util.concurrent.CompletionStages; @@ -72,7 +71,7 @@ import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.UserSessionPersistentChangelogBasedTransaction; import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; @@ -128,7 +127,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi return sessionTx.getCache(offline); } - protected Cache> getClientSessionCache(boolean offline) { + protected Cache> getClientSessionCache(boolean offline) { return clientSessionTx.getCache(offline); } @@ -144,8 +143,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { - final UUID clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSession.getId(), client.getId()); - AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + final EmbeddedClientSessionKey cacheKey = new EmbeddedClientSessionKey(userSession.getId(), client.getId()); + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); entity.setRealmId(realm.getId()); entity.setClientId(client.getId()); entity.setUserSessionId(userSession.getId()); @@ -156,7 +155,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true"); } - AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionTx, false); + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionTx, cacheKey, false); if (userSession.isOffline()) { // If this is an offline session, and the referred online session doesn't exist anymore, don't register the client session in the transaction. @@ -171,8 +170,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi userSession.getPersistenceState() : UserSessionModel.SessionPersistenceState.PERSISTENT; SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); - clientSessionTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState); - sessionTx.registerClientSession(userSession.getId(), client.getId(), clientSessionId, userSession.isOffline()); + clientSessionTx.addTask(cacheKey, createClientSessionTask, entity, persistenceState); + sessionTx.registerClientSession(userSession.getId(), client.getId(), userSession.isOffline()); return adapter; } @@ -298,18 +297,11 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi } @Override - public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, String clientSessionId, boolean offline) { - if (clientSessionId == null) { - log.debugf("Client-session id is null. userSessionId=%s, clientId=%s, offline=%s", - userSession.getId(), client.getId(), offline); - return null; - } - - UUID clientSessionUUID = UUID.fromString(clientSessionId); - - SessionEntityWrapper clientSessionEntity = clientSessionTx.get(client.getRealm(), client, userSession, clientSessionUUID, offline); + public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userSession, ClientModel client, boolean offline) { + var key = new EmbeddedClientSessionKey(userSession.getId(), client.getId()); + SessionEntityWrapper clientSessionEntity = clientSessionTx.get(client.getRealm(), client, userSession, key, offline); if (clientSessionEntity != null) { - return new AuthenticatedClientSessionAdapter(session, clientSessionEntity.getEntity(), client, userSession, clientSessionTx, offline); + return new AuthenticatedClientSessionAdapter(session, clientSessionEntity.getEntity(), client, userSession, clientSessionTx, key, offline); } return null; @@ -434,15 +426,14 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi // public for usage in the testsuite public void removeLocalUserSessions(String realmId, boolean offline) { Cache> localCache = CacheDecorators.localCache(getCache(offline)); - Cache> clientSessionCache = getClientSessionCache(offline); - Cache> localClientSessionCache = CacheDecorators.localCache(clientSessionCache); + var localClientSessionCache = CacheDecorators.localCache(getClientSessionCache(offline)); final AtomicInteger userSessionsSize = new AtomicInteger(); removeEntriesByRealm(realmId, localCache, userSessionsSize, localClientSessionCache); log.debugf("Removed %d sessions in realm %s. Offline: %b", (Object) userSessionsSize.get(), realmId, offline); } - private static void removeEntriesByRealm(String realmId, Cache> sessionsCache, AtomicInteger userSessionsSize, Cache> clientSessions) { + private static void removeEntriesByRealm(String realmId, Cache> sessionsCache, AtomicInteger userSessionsSize, Cache> clientSessions) { FuturesHelper futures = new FuturesHelper(); sessionsCache @@ -456,8 +447,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi // Remove session from remoteCache too. Use removeAsync for better perf Future> future = sessionsCache.removeAsync(userSessionEntity.getId()); futures.addTask(future); - userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { - Future> f = clientSessions.removeAsync(clientSessionId); + userSessionEntity.getClientSessions().forEach(clientUUID -> { + Future> f = clientSessions.removeAsync(new EmbeddedClientSessionKey(userSessionEntity.getId(), clientUUID)); futures.addTask(f); }); }); @@ -512,7 +503,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi } protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) { - sessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> clientSessionTx.addTask(clientSessionId, Tasks.removeSync(offline))); + sessionEntity.getClientSessions().forEach(clientUUID -> clientSessionTx.addTask(new EmbeddedClientSessionKey(sessionEntity.getId(), clientUUID), Tasks.removeSync(offline))); SessionUpdateTask removeTask = Tasks.removeSync(offline); sessionTx.addTask(sessionEntity.getId(), removeTask); } @@ -607,8 +598,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); - offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp())); - offlineClientSession.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted())); + offlineClientSession.setNote(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(offlineClientSession.getTimestamp())); + offlineClientSession.setNote(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(offlineUserSession.getStarted())); return offlineClientSession; } @@ -662,7 +653,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi return null; } - Map> clientSessionsById = new HashMap<>(); + Map> clientSessionsById = new HashMap<>(); for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { String clientUUID = entry.getKey(); @@ -678,11 +669,10 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi clientSessionToImport.setTimestamp(userSessionEntityToImport.getLastSessionRefresh()); } - clientSessionsById.put(clientSessionToImport.getId(), new SessionEntityWrapper<>(clientSessionToImport)); + clientSessionsById.put(new EmbeddedClientSessionKey(persistentUserSession.getId(), clientUUID), new SessionEntityWrapper<>(clientSessionToImport)); // Update userSession entity with the clientSession - AuthenticatedClientSessionStore clientSessions = userSessionEntityToImport.getAuthenticatedClientSessions(); - clientSessions.put(clientUUID, clientSessionToImport.getId()); + userSessionEntityToImport.getClientSessions().add(clientUUID); } SessionEntityWrapper wrappedUserSessionEntity = new SessionEntityWrapper<>(userSessionEntityToImport); @@ -750,7 +740,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); - entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore()); entity.setRememberMe(userSession.isRememberMe()); entity.setState(userSession.getState()); if (userSession instanceof OfflineUserSessionModel offlineUserSession) { @@ -782,16 +771,15 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value entity.setTimestamp(sessionToImportInto.getLastSessionRefresh()); - final UUID clientSessionId = entity.getId(); + var clientUUID = clientSession.getClient().getId(); - SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); - clientSessionTx.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); + var key = new EmbeddedClientSessionKey(sessionToImportInto.getId(), clientUUID); + clientSessionTx.addTask(key, Tasks.addIfAbsentSync(), entity, UserSessionModel.SessionPersistenceState.PERSISTENT); - AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); - clientSessions.put(clientSession.getClient().getId(), clientSessionId); - sessionTx.registerClientSession(sessionToImportInto.getId(), clientSession.getClient().getId(), clientSessionId, true); + sessionToImportInto.getEntity().getClientSessions().add(clientUUID); + sessionTx.registerClientSession(sessionToImportInto.getId(), clientUUID, true); - return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionTx, true); + return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionTx, key, true); } public SessionEntityWrapper wrapPersistentEntity(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) { @@ -821,10 +809,11 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi continue; } + var key = new EmbeddedClientSessionKey(userSessionEntity.getId(), clientUUID); + // Update userSession entity with the clientSession - AuthenticatedClientSessionStore clientSessions = userSessionEntity.getAuthenticatedClientSessions(); - clientSessions.put(clientUUID, clientSession.getId()); - clientSessionTx.addTask(clientSession.getId(), null, clientSession, UserSessionModel.SessionPersistenceState.PERSISTENT); + userSessionEntity.getClientSessions().add(key.clientId()); + clientSessionTx.addTask(key, null, clientSession, UserSessionModel.SessionPersistenceState.PERSISTENT); } return sessionTx.get(userSessionEntity.getId(), offline); @@ -843,11 +832,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi return idleChecker.apply(realm, null, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifetimeChecker.apply(realm, null, entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG; } - public static UUID createClientSessionUUID(String userSessionId, String clientId) { - // This allows creating a UUID that is constant even if the entry is reloaded from the database - return UUID.nameUUIDFromBytes((userSessionId + clientId).getBytes(StandardCharsets.UTF_8)); - } - @Override public void migrate(String modelVersion) { // Changed encoding from JBoss Marshalling to ProtoStream. @@ -855,6 +839,24 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi if ("26.0.0".equals(modelVersion)) { log.debug("Clear caches to migrate to Infinispan Protostream"); CompletionStages.join(session.getProvider(InfinispanConnectionProvider.class).migrateToProtoStream()); + } else if ("26.4.0".equals(modelVersion)) { + log.debug("Clear caches as client session entries are now outdated and are not migrated"); + // This is a best-effort approach: Even if due to a rolling update some entries are left there, the checking of sessions and tokens does not depend on them. + // Refreshing of tokens will still work even if the user session does not contain the list of client sessions. + var stage = CompletionStages.aggregateCompletionStage(); + Stream.of(InfinispanConnectionProvider.USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME, InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME, InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME) + .map(s -> { + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + if (provider != null) { + return provider.getCache(s, false); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .map(AsyncCache::clearAsync) + .forEach(stage::dependsOn); + CompletionStages.join(stage.freeze()); } } @@ -863,11 +865,12 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi * This method is public so people can use it to build their custom migrations or re-import sessions when necessary * in a future version of Keycloak. */ + @Deprecated(since = "26.4", forRemoval = true) public void migrateNonPersistentSessionsToPersistentSessions() { var sessionCache = sessionTx.getCache(false); var clientSessionCache = clientSessionTx.getCache(false); JpaChangesPerformer userSessionPerformer = new JpaChangesPerformer<>(sessionCache.getName(), null); - JpaChangesPerformer clientSessionPerformer = new JpaChangesPerformer<>(clientSessionCache.getName(), null); + JpaChangesPerformer clientSessionPerformer = new JpaChangesPerformer<>(clientSessionCache.getName(), null); AtomicInteger currentBatch = new AtomicInteger(0); var persistence = ComponentRegistry.componentOf(sessionCache, PersistenceManager.class); if (persistence != null && !persistence.getStoresAsString().isEmpty()) { @@ -896,23 +899,25 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi * Such entries should first be cleared from the cache before this is being called. * As this is assumed to run once during the upgrade to Keycloak 25, this should be safe to assume. */ - private void processEntryFromCache(SessionEntityWrapper sessionEntityWrapper, JpaChangesPerformer userSessionPerformer, JpaChangesPerformer clientSessionPerformer, AtomicInteger count) { + private void processEntryFromCache(SessionEntityWrapper sessionEntityWrapper, JpaChangesPerformer userSessionPerformer, JpaChangesPerformer clientSessionPerformer, AtomicInteger count) { RealmModel realm = session.realms().getRealm(sessionEntityWrapper.getEntity().getRealmId()); if (realm == null) { // ignoring old and unknown realm found in the session return; } var clientSessionCache = clientSessionTx.getCache(false); - sessionEntityWrapper.getEntity().getAuthenticatedClientSessions().forEach((clientId, uuid) -> { - SessionEntityWrapper clientSession = clientSessionCache.get(uuid); + sessionEntityWrapper.getEntity().getClientSessions().forEach(clientId-> { + var key = new EmbeddedClientSessionKey(sessionEntityWrapper.getEntity().getId(), clientId); + SessionEntityWrapper clientSession = clientSessionCache.get(key); if (clientSession != null) { + // TODO [pruivo] [KC27] Remove! // This is necessary because client sessions created by a KC version < 22 do not have clientId set within the entity. if (clientSession.getEntity().getClientId() == null) { clientSession.getEntity().setClientId(clientId); } clientSession.getEntity().setUserSessionId(sessionEntityWrapper.getEntity().getId()); MergedUpdate merged = MergedUpdate.computeUpdate(Collections.singletonList(Tasks.addIfAbsentSync()), clientSession, 1, 1); - clientSessionPerformer.registerChange(Map.entry(uuid, new SessionUpdatesList<>(realm, clientSession)), merged); + clientSessionPerformer.registerChange(Map.entry(key, new SessionUpdatesList<>(realm, clientSession)), merged); } }); MergedUpdate merged = MergedUpdate.computeUpdate(Collections.singletonList(Tasks.addIfAbsentSync()), sessionEntityWrapper, 1, 1); @@ -925,7 +930,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi } } - private void flush(JpaChangesPerformer userSessionsPerformer, JpaChangesPerformer clientSessionPerformer) { + private void flush(JpaChangesPerformer userSessionsPerformer, JpaChangesPerformer clientSessionPerformer) { KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), s -> { userSessionsPerformer.write(s); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index 763f6524bfd..bc7b3661a32 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -30,7 +30,7 @@ import org.keycloak.models.sessions.infinispan.changes.SessionsChangelogBasedTra import org.keycloak.models.sessions.infinispan.changes.Tasks; import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.util.Collection; @@ -39,9 +39,6 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.stream.Collectors; /** * @author Stian Thorgersen @@ -56,7 +53,7 @@ public class UserSessionAdapter userSessionUpdateTx; - private final SessionsChangelogBasedTransaction clientSessionUpdateTx; + private final SessionsChangelogBasedTransaction clientSessionUpdateTx; private final RealmModel realm; @@ -70,7 +67,7 @@ public class UserSessionAdapter userSessionUpdateTx, - SessionsChangelogBasedTransaction clientSessionUpdateTx, + SessionsChangelogBasedTransaction clientSessionUpdateTx, RealmModel realm, UserSessionEntity entity, boolean offline) { this.session = session; this.user = user; @@ -84,30 +81,28 @@ public class UserSessionAdapter getAuthenticatedClientSessions() { - AuthenticatedClientSessionStore clientSessionEntities = entity.getAuthenticatedClientSessions(); + var clientSessionEntities = entity.getClientSessions(); Map result = new HashMap<>(); List removedClientUUIDS = new LinkedList<>(); - if (clientSessionEntities != null) { - clientSessionEntities.forEach((String key, UUID value) -> { - // Check if client still exists - ClientModel client = realm.getClientById(key); - if (client != null) { - final AuthenticatedClientSessionModel clientSession = provider.getClientSession(this, client, value.toString(), offline); - if (clientSession != null) { - result.put(key, clientSession); - } else { - // Either the client session has expired, or it hasn't been added by a concurrently running login yet. - // So it is unsafe to clear it, so we need to keep it for now. Otherwise, the test ConcurrentLoginTest.concurrentLoginSingleUser will fail. - // removedClientUUIDS.add(key); - } - } else { - // client does no longer exist - removedClientUUIDS.add(key); - } - }); - } + clientSessionEntities.forEach(clientUUID -> { + // Check if client still exists + ClientModel client = realm.getClientById(clientUUID); + if (client == null) { + // client does no longer exist + removedClientUUIDS.add(clientUUID); + return; + } + var clientSession = provider.getClientSession(this, client, offline); + if (clientSession == null) { + // Either the client session has expired, or it hasn't been added by a concurrently running login yet. + // So it is unsafe to remove it, so we need to keep it for now. + // Otherwise, the test ConcurrentLoginTest.concurrentLoginSingleUser will fail. + return; + } + result.put(clientUUID, clientSession); + }); removeAuthenticatedClientSessions(removedClientUUIDS); @@ -116,25 +111,16 @@ public class UserSessionAdapter clientSessionUuids = removedClientUUIDS.stream() - .map(entity.getAuthenticatedClientSessions()::get) - .filter(Objects::nonNull) + List clientSessionUuids = removedClientUUIDS.stream() + .filter(entity.getClientSessions()::contains) .toList(); // Update user session UserSessionUpdateTask task = new UserSessionUpdateTask() { @Override public void runUpdate(UserSessionEntity entity) { - removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove); + removedClientUUIDS.forEach(entity.getClientSessions()::remove); } @Override @@ -167,7 +152,7 @@ public class UserSessionAdapter this.clientSessionUpdateTx.addTask(clientSessionId, Tasks.removeSync(offline))); + clientSessionUuids.forEach(clientUUID -> this.clientSessionUpdateTx.addTask(new EmbeddedClientSessionKey(entity.getId(), clientUUID), Tasks.removeSync(offline))); } @Override @@ -372,7 +357,7 @@ public class UserSessionAdapter { +public class ClientSessionPersistentChangelogBasedTransaction extends PersistentSessionsChangelogBasedTransaction { private static final Logger LOG = Logger.getLogger(ClientSessionPersistentChangelogBasedTransaction.class); private final UserSessionPersistentChangelogBasedTransaction userSessionTx; public ClientSessionPersistentChangelogBasedTransaction(KeycloakSession session, ArrayBlockingQueue batchingQueue, - CacheHolder cacheHolder, - CacheHolder offlineCacheHolder, + CacheHolder cacheHolder, + CacheHolder offlineCacheHolder, UserSessionPersistentChangelogBasedTransaction userSessionTx) { super(session, CLIENT_SESSION_CACHE_NAME, batchingQueue, cacheHolder, offlineCacheHolder); this.userSessionTx = userSessionTx; } - public void setUserSessionId(Collection keys, String userSessionId, boolean offline) { + public void setUserSessionId(Collection keys, String userSessionId, boolean offline) { keys.stream().map(getUpdates(offline)::get) .filter(Objects::nonNull) .map(SessionUpdatesList::getEntityWrapper) @@ -62,11 +61,14 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent .forEach(authenticatedClientSessionEntity -> authenticatedClientSessionEntity.setUserSessionId(userSessionId)); } - public SessionEntityWrapper get(RealmModel realm, ClientModel client, UserSessionModel userSession, UUID key, boolean offline) { + public SessionEntityWrapper get(RealmModel realm, ClientModel client, UserSessionModel userSession, EmbeddedClientSessionKey key, boolean offline) { + if (key == null) { + key = new EmbeddedClientSessionKey(userSession.getId(), client.getId()); + } SessionUpdatesList myUpdates = getUpdates(offline).get(key); if (myUpdates == null) { SessionEntityWrapper wrappedEntity = null; - Cache> cache = getCache(offline); + Cache> cache = getCache(offline); if (cache != null) { wrappedEntity = cache.get(key); } @@ -116,7 +118,7 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent } } - private SessionEntityWrapper getSessionEntityFromPersister(RealmModel realm, ClientModel client, UserSessionModel userSession, UUID clientSessionId, boolean offline) { + private SessionEntityWrapper getSessionEntityFromPersister(RealmModel realm, ClientModel client, UserSessionModel userSession, EmbeddedClientSessionKey clientSessionId, boolean offline) { UserSessionPersisterProvider persister = kcSession.getProvider(UserSessionPersisterProvider.class); AuthenticatedClientSessionModel clientSession = persister.loadClientSession(realm, client, userSession, offline); @@ -137,9 +139,8 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent public static AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession, String realmId, String clientId, boolean offline) { - UUID clientSessionId = PersistentUserSessionProvider.createClientSessionUUID(userSessionId, clientId); - AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(clientSessionId); + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); entity.setRealmId(realmId); entity.setAction(clientSession.getAction()); @@ -155,7 +156,7 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent return entity; } - private SessionEntityWrapper importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession, UUID clientSessionId) { + private SessionEntityWrapper importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession, EmbeddedClientSessionKey clientSessionId) { AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), persistentClientSession, realm.getId(), client.getId(), userSession.isOffline()); boolean offline = userSession.isOffline(); @@ -194,11 +195,8 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent throw new IllegalStateException("UserSessionModel must be instance of UserSessionAdapter"); } - AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); - UUID existingId = clientSessions.put(client.getId(), clientSessionId); - - if (!Objects.equals(existingId, clientSessionId)) { - userSessionTx.registerClientSession(sessionToImportInto.getId(), client.getClientId(), clientSessionId, offline); + if (sessionToImportInto.getEntity().getClientSessions().add(client.getId())) { + userSessionTx.registerClientSession(sessionToImportInto.getId(), client.getId(), offline); } return wrapper; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java index 90ceae2bd59..f7dfdd17c92 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/JpaChangesPerformer.java @@ -32,7 +32,6 @@ import org.keycloak.models.session.PersistentAuthenticatedClientSessionAdapter; import org.keycloak.models.session.PersistentUserSessionAdapter; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionStore; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.utils.RealmModelDelegate; @@ -46,7 +45,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CompletionStage; @@ -200,7 +198,7 @@ public class JpaChangesPerformer { }; PersistentAuthenticatedClientSessionAdapter clientSessionModel = (PersistentAuthenticatedClientSessionAdapter) userSessionPersister.loadClientSession(realm, client, userSession, entity.isOffline()); if (clientSessionModel != null) { - AuthenticatedClientSessionEntity authenticatedClientSessionEntity = new AuthenticatedClientSessionEntity(entity.getId()) { + AuthenticatedClientSessionEntity authenticatedClientSessionEntity = new AuthenticatedClientSessionEntity() { @Override public Map getNotes() { return new HashMap<>() { @@ -289,11 +287,6 @@ public class JpaChangesPerformer { notes.forEach(clientSessionModel::setNote); } - @Override - public UUID getId() { - return UUID.fromString(clientSessionModel.getId()); - } - @Override public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { throw new IllegalStateException("not implemented"); @@ -341,7 +334,7 @@ public class JpaChangesPerformer { @Override public String getId() { - return entity.getId().toString(); + throw new UnsupportedOperationException(); } @Override @@ -647,16 +640,6 @@ public class JpaChangesPerformer { userSessionModel.setState(state); } - @Override - public AuthenticatedClientSessionStore getAuthenticatedClientSessions() { - return new AuthenticatedClientSessionStore() { - @Override - public void clear() { - userSessionModel.getAuthenticatedClientSessions().clear(); - } - }; - } - @Override public String getRealmId() { return userSessionModel.getRealm().getId(); @@ -738,11 +721,6 @@ public class JpaChangesPerformer { notes.forEach(userSessionModel::setNote); } - @Override - public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) { - throw new IllegalStateException("not supported"); - } - @Override public UserSessionModel.State getState() { return userSessionModel.getState(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java index 5892ff566d5..878a9ba7360 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionPersistentChangelogBasedTransaction.java @@ -28,7 +28,6 @@ import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; -import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; @@ -129,7 +128,7 @@ public class UserSessionPersistentChangelogBasedTransaction extends PersistentSe return isScheduledForRemove(getUpdates(offline).get(key)); } - public void registerClientSession(String userSessionId, String clientId, UUID clientSessionId, boolean offline) { + public void registerClientSession(String userSessionId, String clientId, boolean offline) { addTask(userSessionId, new PersistentSessionUpdateTask<>() { @Override public boolean isOffline() { @@ -138,7 +137,7 @@ public class UserSessionPersistentChangelogBasedTransaction extends PersistentSe @Override public void runUpdate(UserSessionEntity entity) { - entity.getAuthenticatedClientSessions().put(clientId, clientSessionId); + entity.getClientSessions().add(clientId); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java index fb83af033cc..ece888b225a 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -19,11 +19,11 @@ package org.keycloak.models.sessions.infinispan.entities; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.infinispan.protostream.annotations.ProtoFactory; import org.infinispan.protostream.annotations.ProtoField; +import org.infinispan.protostream.annotations.ProtoReserved; import org.infinispan.protostream.annotations.ProtoTypeId; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; @@ -32,19 +32,24 @@ import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; /** * * @author Marek Posolda */ @ProtoTypeId(Marshalling.AUTHENTICATED_CLIENT_SESSION_ENTITY) +@ProtoReserved( + value = {7}, + names = {"id"} +) 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 + @Deprecated(since = "26.4", forRemoval = true) public static final String LAST_TIMESTAMP_REMOTE = "lstr"; + @Deprecated(since = "26.4", forRemoval = true) public static final String CLIENT_ID_NOTE = "clientId"; private String authMethod; @@ -54,12 +59,11 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { private Map notes = new ConcurrentHashMap<>(); - private final UUID id; - + // TODO [pruivo] [KC27] make these fields final. They are the client session identity. private volatile String userSessionId; + private volatile String clientId; - public AuthenticatedClientSessionEntity(UUID id) { - this.id = id; + public AuthenticatedClientSessionEntity() { } @ProtoField(2) @@ -103,12 +107,14 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { return Boolean.parseBoolean(getNotes().get(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE)); } + @ProtoField(9) public String getClientId() { - return getNotes().get(CLIENT_ID_NOTE); + return clientId; } public void setClientId(String clientId) { getNotes().put(CLIENT_ID_NOTE, clientId); + this.clientId = clientId; } @ProtoField(value = 5) @@ -129,70 +135,35 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { this.notes = notes; } - @ProtoField(7) - public UUID getId() { - return id; - } - - @Override - public String toString() { - return "AuthenticatedClientSessionEntity [" + "id=" + id + ']'; - } - @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof AuthenticatedClientSessionEntity that)) { - return false; - } + if (o == null || getClass() != o.getClass()) return false; - return Objects.equals(id, that.id); + AuthenticatedClientSessionEntity that = (AuthenticatedClientSessionEntity) o; + return Objects.equals(userSessionId, that.userSessionId) && Objects.equals(clientId, that.clientId); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(userSessionId); + result = 31 * result + Objects.hashCode(clientId); + return result; } // factory method required because of final fields @ProtoFactory - AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map notes, UUID id) { + AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map notes, String userSessionId, String clientId) { super(realmId); this.authMethod = authMethod; this.redirectUri = redirectUri; this.timestamp = timestamp; this.action = action; this.notes = notes; - this.id = id; - } - - @Override - public int hashCode() { - 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; + this.userSessionId = userSessionId; + this.clientId = clientId; } + @ProtoField(8) public String getUserSessionId() { return userSessionId; } @@ -201,8 +172,8 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { this.userSessionId = userSessionId; } - public static AuthenticatedClientSessionEntity create(UUID clientSessionId, RealmModel realm, ClientModel client, UserSessionModel userSession) { - var entity = new AuthenticatedClientSessionEntity(clientSessionId); + public static AuthenticatedClientSessionEntity create(RealmModel realm, ClientModel client, UserSessionModel userSession) { + var entity = new AuthenticatedClientSessionEntity(); entity.setRealmId(realm.getId()); entity.setClientId(client.getId()); entity.setTimestamp(Time.currentTime()); @@ -215,7 +186,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity { } public static AuthenticatedClientSessionEntity createFromModel(AuthenticatedClientSessionModel model) { - var entity = create(UUID.fromString(model.getId()), model.getRealm(), model.getClient(), model.getUserSession()); + var entity = create(model.getRealm(), model.getClient(), model.getUserSession()); entity.setNotes(model.getNotes() == null ? new ConcurrentHashMap<>() : model.getNotes()); return entity; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/EmbeddedClientSessionKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/EmbeddedClientSessionKey.java new file mode 100644 index 00000000000..2a2dd499f1a --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/EmbeddedClientSessionKey.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.sessions.infinispan.entities; + +import org.infinispan.protostream.annotations.Proto; +import org.infinispan.protostream.annotations.ProtoTypeId; +import org.keycloak.marshalling.Marshalling; + +/** + * The key stored in the {@link org.infinispan.Cache} for {@link AuthenticatedClientSessionEntity}. + *

+ * Although this class is the same as {@link ClientSessionKey}, we keep them separates so they can evolve independent. + */ +@ProtoTypeId(Marshalling.EMBEDDED_CLIENT_SESSION_KEY) +@Proto +public record EmbeddedClientSessionKey(String userSessionId, String clientId) { + + public String toId() { + return userSessionId + "::" + clientId; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java index d2704afb5bc..c56d0c4d215 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java @@ -55,6 +55,8 @@ public abstract class SessionEntity { this.realmId = realmId; } + @Deprecated(since = "26.4", forRemoval = true) + //no longer used public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) { if (localEntityWrapper == null) { return new SessionEntityWrapper<>(this); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java index 8254081e6ff..28410f96a86 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java @@ -17,13 +17,15 @@ package org.keycloak.models.sessions.infinispan.entities; +import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.TreeSet; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.infinispan.protostream.annotations.ProtoFactory; import org.infinispan.protostream.annotations.ProtoField; +import org.infinispan.protostream.annotations.ProtoReserved; import org.infinispan.protostream.annotations.ProtoTypeId; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; @@ -38,6 +40,10 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; * @author Stian Thorgersen */ @ProtoTypeId(Marshalling.USER_SESSION_ENTITY) +@ProtoReserved( + value = {11}, + names = {"authenticatedClientSessions"} +) public class UserSessionEntity extends SessionEntity { public static final Logger logger = Logger.getLogger(UserSessionEntity.class); @@ -66,12 +72,16 @@ public class UserSessionEntity extends SessionEntity { private UserSessionModel.State state; + private final Set clientSessions = ConcurrentHashMap.newKeySet(); + + private Map notes = new ConcurrentHashMap<>(); + public UserSessionEntity(String id) { this.id = id; } @ProtoFactory - static UserSessionEntity protoFactory(String realmId, String id, String user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, int started, int lastSessionRefresh, Map notes, AuthenticatedClientSessionStore authenticatedClientSessions, UserSessionModel.State state, String brokerSessionId, String brokerUserId) { + static UserSessionEntity protoFactory(String realmId, String id, String user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, int started, int lastSessionRefresh, Map notes, UserSessionModel.State state, String brokerSessionId, String brokerUserId, Set clientSessions) { var entity = new UserSessionEntity(id); entity.setRealmId(realmId); entity.setUser(user); @@ -85,7 +95,7 @@ public class UserSessionEntity extends SessionEntity { entity.setBrokerUserId(brokerUserId); entity.setState(state); entity.setNotes(notes); - entity.setAuthenticatedClientSessions(authenticatedClientSessions); + entity.getClientSessions().addAll(clientSessions); return entity; } @@ -94,10 +104,6 @@ public class UserSessionEntity extends SessionEntity { return id; } - private Map notes = new ConcurrentHashMap<>(); - - private AuthenticatedClientSessionStore authenticatedClientSessions = new AuthenticatedClientSessionStore(); - @ProtoField(3) public String getUser() { return user; @@ -170,15 +176,6 @@ public class UserSessionEntity extends SessionEntity { this.notes = notes; } - @ProtoField(11) - public AuthenticatedClientSessionStore getAuthenticatedClientSessions() { - return authenticatedClientSessions; - } - - public void setAuthenticatedClientSessions(AuthenticatedClientSessionStore authenticatedClientSessions) { - this.authenticatedClientSessions = authenticatedClientSessions; - } - @ProtoField(value = 12) public UserSessionModel.State getState() { return state; @@ -206,6 +203,11 @@ public class UserSessionEntity extends SessionEntity { this.brokerUserId = brokerUserId; } + @ProtoField(value = 15, collectionImplementation = HashSet.class) + public Set getClientSessions() { + return clientSessions; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -224,7 +226,7 @@ public class UserSessionEntity extends SessionEntity { @Override public String toString() { return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealmId(), getLastSessionRefresh(), - new TreeSet(this.authenticatedClientSessions.keySet())); + clientSessions); } @Override @@ -283,7 +285,6 @@ public class UserSessionEntity extends SessionEntity { entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setIpAddress(userSession.getIpAddress()); entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes()); - entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore()); entity.setRememberMe(userSession.isRememberMe()); entity.setState(userSession.getState()); if (userSession instanceof OfflineUserSessionModel offline) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remote/RemoteUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remote/RemoteUserSessionProvider.java index 30ac07cf77e..2b49c62ecfd 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remote/RemoteUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remote/RemoteUserSessionProvider.java @@ -95,10 +95,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider { } @Override - public AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, String clientSessionId, boolean offline) { - if (clientSessionId == null) { - return null; - } + public AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, boolean offline) { var clientTx = getClientSessionTransaction(offline); var updater = clientTx.get(new ClientSessionKey(userSession.getId(), client.getId())); if (updater == null) { @@ -317,7 +314,7 @@ public class RemoteUserSessionProvider implements UserSessionProvider { userSessionBuffer.add(userSessionModel.getId()); for (var clientSessionModel : userSessionModel.getAuthenticatedClientSessions().values()) { var clientSessionKey = new ClientSessionKey(userSessionModel.getId(), clientSessionModel.getClient().getId()); - clientSessionBuffer.add(Map.entry(userSessionModel.getId(), clientSessionModel.getId())); + clientSessionBuffer.add(Map.entry(clientSessionKey.userSessionId(), clientSessionKey.clientId())); var clientSessionEntity = RemoteAuthenticatedClientSessionEntity.createFromModel(clientSessionKey, clientSessionModel); stage.dependsOn(clientSessionCache.putIfAbsentAsync(clientSessionKey, clientSessionEntity)); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthClientSessionSetMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthClientSessionSetMapper.java index ce72d9bf245..93c1930fc26 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthClientSessionSetMapper.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthClientSessionSetMapper.java @@ -49,6 +49,6 @@ public class AuthClientSessionSetMapper implements Function apply(Map.Entry> entry) { - return entry.getValue().getEntity().getAuthenticatedClientSessions().keySet(); + return entry.getValue().getEntity().getClientSessions(); } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java index 96bae2e23da..feea6e858bb 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -130,7 +130,7 @@ public class UserSessionPredicate implements Predicate persistentUserSessions, boolean offline) {} void close(); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index d05241d52f7..a5e412d7882 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -25,7 +25,6 @@ import org.keycloak.cookie.CookieType; import org.keycloak.http.HttpRequest; import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; -import org.keycloak.TokenVerifier.Predicate; import org.keycloak.TokenVerifier.TokenTypeCheck; import org.keycloak.authentication.AuthenticationFlowException; import org.keycloak.authentication.AuthenticationProcessor; @@ -109,7 +108,6 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -271,7 +269,7 @@ public class AuthenticationManager { ClientConnection connection, HttpHeaders headers, boolean logoutBroker) { - return backchannelLogout(session, realm, userSession, uriInfo, connection, headers, logoutBroker, userSession == null ? false : userSession.isOffline()); + return backchannelLogout(session, realm, userSession, uriInfo, connection, headers, logoutBroker, userSession != null && userSession.isOffline()); } /** @@ -1161,7 +1159,7 @@ public class AuthenticationManager { getClientScopesToApproveOnConsentScreen(grantedConsent, session, authSession); // Skip grant screen if everything was already approved by this user - if (clientScopesToApprove.size() > 0) { + if (!clientScopesToApprove.isEmpty()) { String execution = AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name(); ClientSessionCode accessCode = @@ -1727,7 +1725,7 @@ public class AuthenticationManager { Map clientSessions = userSession.getAuthenticatedClientSessions(); return clientSessions.values().stream().filter(c -> c.getClient().equals(client)) - .map((c) -> c.getNotes().get(OIDCLoginProtocol.SCOPE_PARAM)) + .map((c) -> c.getNote(OIDCLoginProtocol.SCOPE_PARAM)) .filter(Objects::nonNull) .findFirst() .orElse(null); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java index 78174fc4d5e..96544e7bffc 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionPersisterProviderTest.java @@ -25,10 +25,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.hamcrest.Matchers; @@ -58,6 +56,7 @@ import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.PersistentUserSessionProvider; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.EmbeddedClientSessionKey; import org.keycloak.models.utils.ResetTimeOffsetEvent; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; @@ -159,7 +158,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { RealmModel realm = session.realms().getRealm(realmId); session.getContext().setRealm(realm); ClientModel testApp = realm.getClientByClientId("test-app"); - session.sessions().getUserSessionsStream(realm, testApp).collect(Collectors.toList()) + session.sessions().getUserSessionsStream(realm, testApp).toList() .forEach(userSessionLooper -> persistUserSession(session, userSessionLooper, true)); }); @@ -195,10 +194,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { int started = Time.currentTime(); AtomicReference origSessionsAt = new AtomicReference<>(); - AtomicReference> loadedSessionsAt = new AtomicReference<>(); - AtomicReference userSessionAt = new AtomicReference<>(); - AtomicReference persistedSessionAt = new AtomicReference<>(); inComittedTransaction(session -> { // Create some sessions in infinispan @@ -225,10 +221,8 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { // Load offline session List loadedSessions = loadPersistedSessionsPaginated(session, true, 10, 1, 1); - loadedSessionsAt.set(loadedSessions); UserSessionModel persistedSession = loadedSessions.get(0); - persistedSessionAt.set(persistedSession); assertSession(persistedSession, session.users().getUserByUsername(realm, "user1"), "127.0.0.2", started, started, "test-app"); @@ -391,7 +385,6 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { final String username = "my-user"; final String clientId = "my-app"; final AtomicReference userSessionID = new AtomicReference<>(); - final AtomicReference clientSessionId = new AtomicReference<>(); // create user and client inComittedTransaction(session -> { @@ -405,8 +398,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { UserSessionModel userSession = session.sessions().createUserSession(null, realm, session.users().getUserByUsername(realm, username), username, "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); userSessionID.set(userSession.getId()); - AuthenticatedClientSessionModel clientSession = createClientSession(session, realm.getId(), realm.getClientByClientId(clientId), userSession, "http://redirect", "state"); - clientSessionId.set(clientSession.getId()); + createClientSession(session, realm.getId(), realm.getClientByClientId(clientId), userSession, "http://redirect", "state"); }); if (InfinispanUtils.isEmbeddedInfinispan()) { @@ -415,8 +407,9 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { RealmModel realm = session.realms().getRealmByName(realmName); session.getContext().setRealm(realm); - Cache> clientSessoinCache = session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME); - SessionEntityWrapper clientSession = clientSessoinCache.get(UUID.fromString(clientSessionId.get())); + var cacheKey = new EmbeddedClientSessionKey(userSessionID.get(), realm.getClientByClientId(clientId).getId()); + Cache> clientSessoinCache = session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME); + SessionEntityWrapper clientSession = clientSessoinCache.get(cacheKey); assertNotNull(clientSession); assertNotNull(clientSession.getEntity()); // user session id is not stored in the cache @@ -440,7 +433,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { .getTimestamp(); } return session.sessions() - .getClientSession(userSession, client, clientSessionId.get(), false) + .getClientSession(userSession, client, false) .getTimestamp(); }; @@ -455,7 +448,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { ClientModel client = realm.getClientByClientId(clientId); UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionID.get()); session.sessions() - .getClientSession(userSession, client, clientSessionId.get(), false) + .getClientSession(userSession, client, false) .setTimestamp(currentTimestamp + 10); }); @@ -723,6 +716,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { } @Test + @Deprecated(since = "26.4", forRemoval = true) public void testMigrateSession() { Assume.assumeTrue(MultiSiteUtils.isPersistentSessionsEnabled()); Assume.assumeTrue(InfinispanUtils.isEmbeddedInfinispan()); @@ -745,7 +739,6 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { // trigger a migration with the entries that are still in the cache PersistentUserSessionProvider userSessionProvider = (PersistentUserSessionProvider) session.getProvider(UserSessionProvider.class); userSessionProvider.migrateNonPersistentSessionsToPersistentSessions(); - JpaUserSessionPersisterProvider sessionPersisterProvider = (JpaUserSessionPersisterProvider) session.getProvider(UserSessionPersisterProvider.class); // verify that import was complete Assert.assertEquals(sessions.length, countUserSessionsInRealm(session)); @@ -830,8 +823,6 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { private List loadPersistedSessionsPaginated(KeycloakSession session, boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - int count = persister.getUserSessionsCount(offline); - int pageCount = 0; boolean next = true; List result = new ArrayList<>(); @@ -840,13 +831,13 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest { while (next) { List sess = persister .loadUserSessionsStream(0, sessionsPerPage, offline, lastSessionId) - .collect(Collectors.toList()); + .toList(); if (sess.size() < sessionsPerPage) { next = false; // We had at least some session - if (sess.size() > 0) { + if (!sess.isEmpty()) { pageCount++; } } else { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java index bf18a5e7777..ef644c58bf5 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/session/UserSessionProviderModelTest.java @@ -17,7 +17,6 @@ package org.keycloak.testsuite.model.session; import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.ConcurrentHashMap; @@ -25,8 +24,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import org.hamcrest.Matchers; import org.junit.Assert; @@ -151,9 +148,6 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { return createSessions(session, realmId); }); - AtomicReference> clientSessionIds = new AtomicReference<>(); - clientSessionIds.set(origSessions[0].getAuthenticatedClientSessions().values().stream().map(AuthenticatedClientSessionModel::getId).collect(Collectors.toList())); - inComittedTransaction(session -> { RealmModel realm = session.realms().getRealm(realmId); session.getContext().setRealm(realm); @@ -162,7 +156,6 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { Assert.assertEquals(origSessions[0], userSession); AuthenticatedClientSessionModel clientSession = session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), - origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId(), false); Assert.assertEquals(origSessions[0].getAuthenticatedClientSessionByClient(realm.getClientByClientId("test-app").getId()).getId(), clientSession.getId()); @@ -182,10 +175,8 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { Assert.assertEquals(origSessions[0], userSession); // assert the client sessions are expired - clientSessionIds.get().forEach(clientSessionId -> { - Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), clientSessionId, false)); - Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("third-party"), clientSessionId, false)); - }); + Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("test-app"), false)); + Assert.assertNull(session.sessions().getClientSession(userSession, realm.getClientByClientId("third-party"), false)); }); } finally { setTimeOffset(0); @@ -207,10 +198,10 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT); ClientModel testApp = realm.getClientByClientId("test-app"); - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp, userSession); + session.sessions().createClientSession(realm, testApp, userSession); // assert the client sessions are present - assertThat(session.sessions().getClientSession(userSession, testApp, clientSession.getId(), false), notNullValue()); + assertThat(session.sessions().getClientSession(userSession, testApp, false), notNullValue()); return userSession.getId(); }); @@ -225,27 +216,22 @@ public class UserSessionProviderModelTest extends KeycloakModelTest { @Test public void testClientSessionIsNotPersistedForTransientUserSession() { - Object[] transientUserSessionWithClientSessionId = inComittedTransaction(session -> { + UserSessionModel userSession = inComittedTransaction(session -> { RealmModel realm = session.realms().getRealm(realmId); session.getContext().setRealm(realm); - UserSessionModel userSession = session.sessions().createUserSession(null, realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT); + UserSessionModel us = session.sessions().createUserSession(null, realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT); ClientModel testApp = realm.getClientByClientId("test-app"); - AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp, userSession); + session.sessions().createClientSession(realm, testApp, us); // assert the client sessions are present - assertThat(session.sessions().getClientSession(userSession, testApp, clientSession.getId(), false), notNullValue()); - Object[] result = new Object[2]; - result[0] = userSession; - result[1] = clientSession.getId(); - return result; + assertThat(session.sessions().getClientSession(us, testApp, false), notNullValue()); + return us; }); inComittedTransaction(session -> { RealmModel realm = session.realms().getRealm(realmId); ClientModel testApp = realm.getClientByClientId("test-app"); - UserSessionModel userSession = (UserSessionModel) transientUserSessionWithClientSessionId[0]; - String clientSessionId = (String) transientUserSessionWithClientSessionId[1]; // in new transaction transient session should not be present - assertThat(session.sessions().getClientSession(userSession, testApp, clientSessionId, false), nullValue()); + assertThat(session.sessions().getClientSession(userSession, testApp, false), nullValue()); }); } diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java index 708d040f2f8..2342d39568b 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/util/cli/AbstractSessionCacheCommand.java @@ -83,7 +83,7 @@ public abstract class AbstractSessionCacheCommand extends AbstractCommand { } protected String toString(UserSessionEntity userSession) { - int clientSessionsSize = userSession.getAuthenticatedClientSessions()==null ? 0 : userSession.getAuthenticatedClientSessions().size(); + int clientSessionsSize = userSession.getClientSessions().size(); return "ID: " + userSession.getId() + ", realm: " + userSession.getRealmId()+ ", lastAccessTime: " + Time.toDate(userSession.getLastSessionRefresh()) + ", authenticatedClientSessions: " + clientSessionsSize; }