* Docs: Language Guide (proto 3)
*/
+@SuppressWarnings("unused")
public final class Marshalling {
public static final String PROTO_SCHEMA_PACKAGE = "keycloak";
@@ -183,6 +184,8 @@ public final class Marshalling {
public static final int RELOAD_CERTIFICATE_FUNCTION = 65615;
public static final int EMBEDDED_CLIENT_SESSION_KEY = 65616;
+ public static final int CLIENT_SESSION_USER_FILTER = 65617;
+ public static final int REMOVE_KEY_BI_CONSUMER = 65618;
public static void configure(GlobalConfigurationBuilder builder) {
getSchemas().forEach(builder.serialization()::addContextInitializer);
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 1b59ad6b10b..c069ad8bcb7 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
@@ -906,6 +906,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi
entity.setRealmId(realmId);
entity.setClientId(clientId);
entity.setUserSessionId(clientSession.getUserSession().getId());
+ entity.setUserId(clientSession.getUserSession().getId());
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());
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 78f0541b713..e6da21c3320 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
@@ -78,7 +78,10 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
+import org.keycloak.models.sessions.infinispan.stream.ClientSessionFilterByUser;
+import org.keycloak.models.sessions.infinispan.stream.MapEntryToKeyMapper;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
+import org.keycloak.models.sessions.infinispan.stream.RemoveKeyConsumer;
import org.keycloak.models.sessions.infinispan.stream.SessionWrapperPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.FuturesHelper;
@@ -107,6 +110,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
protected final ClientSessionPersistentChangelogBasedTransaction clientSessionTx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
+ protected final UserSessionPersisterProvider userSessionPersister;
public PersistentUserSessionProvider(KeycloakSession session,
UserSessionPersistentChangelogBasedTransaction sessionTx,
@@ -121,6 +125,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
+ userSessionPersister = session.getProvider(UserSessionPersisterProvider.class);
}
protected Cache> getCache(boolean offline) {
@@ -148,6 +153,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
entity.setRealmId(realm.getId());
entity.setClientId(client.getId());
entity.setUserSessionId(userSession.getId());
+ entity.setUserId(userSession.getUser().getId());
entity.setTimestamp(Time.currentTime());
entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp()));
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE, String.valueOf(userSession.getStarted()));
@@ -392,7 +398,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
}
protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) {
- UserSessionPredicate.create(realm.getId()).user(user.getId());
getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).user(user.getId()), offline)
.forEach(s -> removeUserSession(realm, s));
}
@@ -485,13 +490,9 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
}
protected void onUserRemoved(RealmModel realm, UserModel user) {
- removeUserSessions(realm, user, true);
- removeUserSessions(realm, user, false);
-
- UserSessionPersisterProvider persisterProvider = session.getProvider(UserSessionPersisterProvider.class);
- if (persisterProvider != null) {
- persisterProvider.onUserRemoved(realm, user);
- }
+ userSessionPersister.onUserRemoved(realm, user);
+ removeCachedUserAndClientSessionForUser(realm.getId(), user.getId(), true);
+ removeCachedUserAndClientSessionForUser(realm.getId(), user.getId(), false);
}
@Override
@@ -640,6 +641,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
UserSessionEntity userSessionEntityToImport = createUserSessionEntityInstance(persistentUserSession);
String realmId = userSessionEntityToImport.getRealmId();
String sessionId = userSessionEntityToImport.getId();
+ String userId = userSessionEntityToImport.getUser();
RealmModel realm = session.realms().getRealm(realmId);
long lifespan = offline ?
@@ -660,9 +662,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
String clientUUID = entry.getKey();
AuthenticatedClientSessionModel clientSession = entry.getValue();
- AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(sessionId, clientSession,
+ AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(sessionId, userId, clientSession,
realmId, clientUUID, offline);
- clientSessionToImport.setUserSessionId(sessionId);
if (offline) {
// Update timestamp to the same value as userSession. LastSessionRefresh of userSession from DB will have a correct value.
@@ -766,9 +767,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
private AuthenticatedClientSessionAdapter importOfflineClientSession(UserSessionAdapter> sessionToImportInto,
AuthenticatedClientSessionModel clientSession) {
- AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(sessionToImportInto.getId(), clientSession,
+ AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(sessionToImportInto.getId(), sessionToImportInto.getUser().getId(), clientSession,
sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), true);
- entity.setUserSessionId(sessionToImportInto.getId());
// Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value
entity.setTimestamp(sessionToImportInto.getLastSessionRefresh());
@@ -795,9 +795,8 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) {
String clientUUID = entry.getKey();
- AuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(persistentUserSession.getId(), entry.getValue(),
+ AuthenticatedClientSessionEntity clientSession = createAuthenticatedClientSessionInstance(persistentUserSession.getId(), userSessionEntity.getUser(), entry.getValue(),
userSessionEntity.getRealmId(), clientUUID, offline);
- clientSession.setUserSessionId(userSessionEntity.getId());
if (offline) {
// Update timestamp to the same value as userSession. LastSessionRefresh of userSession from DB will have a correct value.
@@ -918,6 +917,7 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
clientSession.getEntity().setClientId(clientId);
}
clientSession.getEntity().setUserSessionId(sessionEntityWrapper.getEntity().getId());
+ clientSession.getEntity().setUserId(sessionEntityWrapper.getEntity().getUser());
MergedUpdate merged = MergedUpdate.computeUpdate(Collections.singletonList(Tasks.addIfAbsentSync()), clientSession, 1, 1);
clientSessionPerformer.registerChange(Map.entry(key, new SessionUpdatesList<>(realm, clientSession)), merged);
}
@@ -956,4 +956,25 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
private void addClientSessionToUserSession(EmbeddedClientSessionKey cacheKey, boolean offline) {
sessionTx.registerClientSession(cacheKey.userSessionId(), cacheKey.clientId(), offline);
}
+
+ private void removeCachedUserAndClientSessionForUser(String realmId, String userId, boolean offline) {
+ if (getCache(offline) == null) {
+ // caching disabled
+ return;
+ }
+ try (var stream = getCache(offline).getAdvancedCache()
+ .entrySet()
+ .stream()
+ .filter(UserSessionPredicate.create(realmId).user(userId))
+ .map(MapEntryToKeyMapper.getInstance())) {
+ stream.forEach(RemoveKeyConsumer.getInstance());
+ }
+ try (var stream = getClientSessionCache(offline) .getAdvancedCache()
+ .entrySet()
+ .stream()
+ .filter(new ClientSessionFilterByUser(realmId, userId))
+ .map(MapEntryToKeyMapper.getInstance())) {
+ stream.forEach(RemoveKeyConsumer.getInstance());
+ }
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java
index 016331402e3..8cd4f3cd43e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/ClientSessionPersistentChangelogBasedTransaction.java
@@ -136,7 +136,7 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
return authenticatedClientSessionEntitySessionEntityWrapper;
}
- public static AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, AuthenticatedClientSessionModel clientSession,
+ public static AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstance(String userSessionId, String userId, AuthenticatedClientSessionModel clientSession,
String realmId, String clientId, boolean offline) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
@@ -151,12 +151,13 @@ public class ClientSessionPersistentChangelogBasedTransaction extends Persistent
entity.setTimestamp(clientSession.getTimestamp());
entity.setOffline(offline);
entity.setUserSessionId(userSessionId);
+ entity.setUserId(userId);
return entity;
}
private SessionEntityWrapper importClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession, AuthenticatedClientSessionModel persistentClientSession, EmbeddedClientSessionKey clientSessionId) {
- AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), persistentClientSession,
+ AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(userSession.getId(), userSession.getUser().getId(), persistentClientSession,
realm.getId(), client.getId(), userSession.isOffline());
boolean offline = userSession.isOffline();
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 ece888b225a..24ea0776208 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
@@ -25,7 +25,6 @@ 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;
import org.keycloak.marshalling.Marshalling;
import org.keycloak.models.AuthenticatedClientSessionModel;
@@ -44,8 +43,6 @@ import org.keycloak.models.UserSessionModel;
)
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";
@@ -62,6 +59,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
// TODO [pruivo] [KC27] make these fields final. They are the client session identity.
private volatile String userSessionId;
private volatile String clientId;
+ private volatile String userId;
public AuthenticatedClientSessionEntity() {
}
@@ -117,7 +115,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.clientId = clientId;
}
- @ProtoField(value = 5)
+ @ProtoField(5)
public String getAction() {
return action;
}
@@ -135,6 +133,15 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.notes = notes;
}
+ @ProtoField(10)
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
@@ -152,7 +159,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
// factory method required because of final fields
@ProtoFactory
- AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map notes, String userSessionId, String clientId) {
+ AuthenticatedClientSessionEntity(String realmId, String authMethod, String redirectUri, int timestamp, String action, Map notes, String userSessionId, String clientId, String userId) {
super(realmId);
this.authMethod = authMethod;
this.redirectUri = redirectUri;
@@ -161,6 +168,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.notes = notes;
this.userSessionId = userSessionId;
this.clientId = clientId;
+ this.userId = userId;
}
@ProtoField(8)
@@ -182,6 +190,7 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
if (userSession.isRememberMe()) {
entity.getNotes().put(AuthenticatedClientSessionModel.USER_SESSION_REMEMBER_ME_NOTE, "true");
}
+ entity.setUserId(userSession.getUser().getId());
return entity;
}
@@ -190,5 +199,4 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
entity.setNotes(model.getNotes() == null ? new ConcurrentHashMap<>() : model.getNotes());
return entity;
}
-
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionFilterByUser.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionFilterByUser.java
new file mode 100644
index 00000000000..344afb3604e
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionFilterByUser.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2025 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.stream;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.infinispan.protostream.annotations.Proto;
+import org.infinispan.protostream.annotations.ProtoTypeId;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+
+import static org.keycloak.marshalling.Marshalling.CLIENT_SESSION_USER_FILTER;
+
+/**
+ * A {@link Predicate} to filter {@link AuthenticatedClientSessionEntity} values based on the Realm ID and the User ID.
+ *
+ * @param realmId The Realm ID.
+ * @param userId The User ID.
+ */
+@ProtoTypeId(CLIENT_SESSION_USER_FILTER)
+@Proto
+public record ClientSessionFilterByUser(String realmId,
+ String userId) implements Predicate>> {
+
+ @Override
+ public boolean test(Map.Entry, SessionEntityWrapper> entry) {
+ var entity = entry.getValue().getEntity();
+ return Objects.equals(userId, entity.getUserId()) &&
+ Objects.equals(realmId, entity.getRealmId());
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RemoveKeyConsumer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RemoveKeyConsumer.java
new file mode 100644
index 00000000000..db00135cebe
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RemoveKeyConsumer.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 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.stream;
+
+import java.util.function.BiConsumer;
+
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.infinispan.protostream.annotations.ProtoFactory;
+import org.infinispan.protostream.annotations.ProtoTypeId;
+
+import static org.keycloak.marshalling.Marshalling.REMOVE_KEY_BI_CONSUMER;
+
+/**
+ * Removes keys from a {@link Cache}.
+ *
+ * This implementation is best-effortly, meaning if the removal fails, it won't throw any exception.
+ *
+ * @param The type of key stored in the cache.
+ * @param The type of the value store in the cache.
+ */
+@ProtoTypeId(REMOVE_KEY_BI_CONSUMER)
+public class RemoveKeyConsumer implements BiConsumer, K> {
+
+ private static final RemoveKeyConsumer