mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
User session deleted events for invalid sessions
Closes #44513 Signed-off-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com> Co-authored-by: Pedro Ruivo <1492066+pruivo@users.noreply.github.com>
This commit is contained in:
parent
aa789dd023
commit
b35dd72392
@ -62,7 +62,7 @@ abstract class BaseUserSessionExpirationListener {
|
||||
.session(userSessionId)
|
||||
.user(userId)
|
||||
.event(EventType.USER_SESSION_DELETED)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.detail(Details.REASON, Details.USER_SESSION_EXPIRED_REASON)
|
||||
.success();
|
||||
});
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||
if (realm.isRememberMe()) {
|
||||
UserSessionExpirationLogic.expireRegularSessions(sessionFactory, realm, currentTime, expiration, true, expirationBatch);
|
||||
} else {
|
||||
UserSessionExpirationLogic.deleteInvalidSessions(sessionFactory, realm);
|
||||
UserSessionExpirationLogic.deleteInvalidSessions(sessionFactory, realm, expirationBatch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -68,11 +68,11 @@ import org.hibernate.annotations.DynamicUpdate;
|
||||
" WHERE sess.offline = :offline AND sess.realmId = :realmId AND sess.lastSessionRefresh >= :lastSessionRefresh" +
|
||||
" GROUP BY clientSess.clientId, clientSess.externalClientId, clientSess.clientStorageProvider"),
|
||||
@NamedQuery(name = "findUserSessionAndDataWithNullRememberMeLastRefresh",
|
||||
query = "SELECT sess.userSessionId, sess.data" +
|
||||
query = "SELECT sess.userSessionId, sess.userId, sess.data" +
|
||||
" FROM PersistentUserSessionEntity sess" +
|
||||
" WHERE sess.realmId = :realmId AND sess.offline = '0' AND sess.rememberMe IS NULL AND sess.lastSessionRefresh < :lastSessionRefresh"),
|
||||
@NamedQuery(name = "findUserSessionAndDataWithNullRememberMeCreatedOn",
|
||||
query = "SELECT sess.userSessionId, sess.data" +
|
||||
query = "SELECT sess.userSessionId, sess.userId, sess.data" +
|
||||
" FROM PersistentUserSessionEntity sess" +
|
||||
" WHERE sess.realmId = :realmId AND sess.offline = '0' AND sess.rememberMe IS NULL AND sess.createdOn < :createdOn"),
|
||||
@NamedQuery(name = "updateUserSessionRememberMeColumn",
|
||||
@ -95,8 +95,9 @@ import org.hibernate.annotations.DynamicUpdate;
|
||||
query = "SELECT sess.userSessionId, sess.userId" +
|
||||
" FROM PersistentUserSessionEntity sess" +
|
||||
" WHERE sess.realmId = :realmId AND sess.offline = '0' AND sess.rememberMe = :rememberMe AND sess.createdOn < :createdOn"),
|
||||
@NamedQuery(name = "deleteInvalidSessions",
|
||||
query = "DELETE FROM PersistentUserSessionEntity sess" +
|
||||
@NamedQuery(name = "findInvalidRegularUserSessions",
|
||||
query = "SELECT sess.userSessionId, sess.userId" +
|
||||
" FROM PersistentUserSessionEntity sess" +
|
||||
" WHERE sess.realmId = :realmId AND sess.offline = '0' AND sess.rememberMe = true"),
|
||||
|
||||
})
|
||||
|
||||
@ -34,6 +34,7 @@ import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.RealmExpiration;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
@ -51,6 +52,8 @@ import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransactionWi
|
||||
final class UserSessionExpirationLogic {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
|
||||
private static final Consumer<TypedQuery<Object[]>> NO_PARAMETERS = typedQuery -> {
|
||||
};
|
||||
|
||||
private UserSessionExpirationLogic() {
|
||||
}
|
||||
@ -80,19 +83,14 @@ final class UserSessionExpirationLogic {
|
||||
String realmId = realm.getId();
|
||||
final List<UserSessionAndUser> expiredSessions = new ArrayList<>(batchSize);
|
||||
|
||||
boolean hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> removeExpiredOfflineSessionsInTransaction(s, realmId, batchSize, "findExpiredOfflineUserSessionsLastRefresh", setLastSessionRefresh, expiredSessions));
|
||||
expiredSessions.clear();
|
||||
}
|
||||
runInBatches(sessionFactory,
|
||||
s -> findAndRemoveSessions(s, realmId, batchSize, true, Details.USER_SESSION_EXPIRED_REASON, "findExpiredOfflineUserSessionsLastRefresh", setLastSessionRefresh, expiredSessions),
|
||||
expiredSessions::clear);
|
||||
|
||||
runInBatches(sessionFactory,
|
||||
s -> findAndRemoveSessions(s, realmId, batchSize, true, Details.USER_SESSION_EXPIRED_REASON, "findExpiredOfflineUserSessionsCreatedOn", setCreatedOn, expiredSessions),
|
||||
expiredSessions::clear);
|
||||
|
||||
hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> removeExpiredOfflineSessionsInTransaction(s, realmId, batchSize, "findExpiredOfflineUserSessionsCreatedOn", setCreatedOn, expiredSessions));
|
||||
expiredSessions.clear();
|
||||
}
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
logger.debugf("Offline user session expiration task completed for realm '%s'. Took %dms", realm.getName(), TimeUnit.NANOSECONDS.toMillis(duration));
|
||||
@ -114,27 +112,24 @@ final class UserSessionExpirationLogic {
|
||||
logger.tracef("Removing expired regular user sessions for realm '%s'", realm.getName());
|
||||
|
||||
int oldestCreatedOn = currentTime - expiration.getLifespan(rememberMe) - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
Consumer<TypedQuery<Object[]>> setCreatedOn = setCreatedOn(oldestCreatedOn);
|
||||
Consumer<TypedQuery<Object[]>> setRememberMe = setRememberMe(rememberMe);
|
||||
Consumer<TypedQuery<Object[]>> setCreatedOn = UserSessionExpirationLogic.<Object[]>setCreatedOn(oldestCreatedOn)
|
||||
.andThen(setRememberMe);
|
||||
|
||||
int oldestLastSessionRefresh = currentTime - expiration.getMaxIdle(rememberMe) - SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS;
|
||||
Consumer<TypedQuery<Object[]>> setLastSessionRefresh = setLastSessionRefresh(oldestLastSessionRefresh);
|
||||
var setLastSessionRefresh = UserSessionExpirationLogic.<Object[]>setLastSessionRefresh(oldestLastSessionRefresh)
|
||||
.andThen(setRememberMe);
|
||||
|
||||
String realmId = realm.getId();
|
||||
final List<UserSessionAndUser> expiredSessions = new ArrayList<>(batchSize);
|
||||
|
||||
boolean hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> removeExpiredRegularSessionInTransaction(s, realmId, rememberMe, batchSize, "findExpiredRegularUserSessionsLastRefresh", setLastSessionRefresh, expiredSessions));
|
||||
expiredSessions.clear();
|
||||
}
|
||||
runInBatches(sessionFactory,
|
||||
s -> findAndRemoveSessions(s, realmId, batchSize, false, Details.USER_SESSION_EXPIRED_REASON, "findExpiredRegularUserSessionsLastRefresh", setLastSessionRefresh, expiredSessions),
|
||||
expiredSessions::clear);
|
||||
|
||||
hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> removeExpiredRegularSessionInTransaction(s, realmId, rememberMe, batchSize, "findExpiredRegularUserSessionsCreatedOn", setCreatedOn, expiredSessions));
|
||||
expiredSessions.clear();
|
||||
}
|
||||
runInBatches(sessionFactory,
|
||||
s -> findAndRemoveSessions(s, realmId, batchSize, false, Details.USER_SESSION_EXPIRED_REASON, "findExpiredRegularUserSessionsCreatedOn", setCreatedOn, expiredSessions),
|
||||
expiredSessions::clear);
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
logger.debugf("Regular user session expiration task completed for realm '%s'. Took %dms", realm.getName(), TimeUnit.NANOSECONDS.toMillis(duration));
|
||||
@ -166,23 +161,18 @@ final class UserSessionExpirationLogic {
|
||||
final int expireLifespan = currentTime - Math.min(expiration.lifespan(), expiration.rememberMeLifespan());
|
||||
Consumer<TypedQuery<Object[]>> setCreatedOn = setCreatedOn(expireLifespan);
|
||||
|
||||
final List<String> sessionsWithRememberMe = new ArrayList<>(batchSize);
|
||||
final List<UserSessionAndUser> sessionsWithRememberMe = new ArrayList<>(batchSize);
|
||||
final List<String> sessionsWithoutRememberMe = new ArrayList<>(batchSize);
|
||||
boolean hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> handleRememberMeColumnValue(s, realmId, realmName, batchSize, rememberMeEnabledInRealm, "findUserSessionAndDataWithNullRememberMeLastRefresh", setLastSessionRefresh, sessionsWithRememberMe, sessionsWithoutRememberMe));
|
||||
final Runnable cleanup = () -> {
|
||||
sessionsWithRememberMe.clear();
|
||||
sessionsWithoutRememberMe.clear();
|
||||
}
|
||||
|
||||
hasMore = true;
|
||||
while (hasMore) {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory,
|
||||
s -> handleRememberMeColumnValue(s, realmId, realmName, batchSize, rememberMeEnabledInRealm, "findUserSessionAndDataWithNullRememberMeCreatedOn", setCreatedOn, sessionsWithRememberMe, sessionsWithoutRememberMe));
|
||||
sessionsWithRememberMe.clear();
|
||||
sessionsWithoutRememberMe.clear();
|
||||
}
|
||||
};
|
||||
runInBatches(sessionFactory,
|
||||
s -> handleRememberMeColumnValue(s, realmId, realmName, batchSize, rememberMeEnabledInRealm, "findUserSessionAndDataWithNullRememberMeLastRefresh", setLastSessionRefresh, sessionsWithRememberMe, sessionsWithoutRememberMe),
|
||||
cleanup);
|
||||
runInBatches(sessionFactory,
|
||||
s -> handleRememberMeColumnValue(s, realmId, realmName, batchSize, rememberMeEnabledInRealm, "findUserSessionAndDataWithNullRememberMeCreatedOn", setCreatedOn, sessionsWithRememberMe, sessionsWithoutRememberMe),
|
||||
cleanup);
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
logger.debugf("Migration task completed for realm '%s'. Took %dms", realmName, TimeUnit.NANOSECONDS.toMillis(duration));
|
||||
@ -196,25 +186,25 @@ final class UserSessionExpirationLogic {
|
||||
*
|
||||
* @param sessionFactory The {@link KeycloakSessionFactory}, used to start transactions.
|
||||
* @param realm The {@link RealmModel} to check and remove invalid user sessions.
|
||||
* @param batchSize Sets the maximum number of user sessions to remove in a single transaction.
|
||||
*/
|
||||
public static void deleteInvalidSessions(KeycloakSessionFactory sessionFactory, RealmModel realm) {
|
||||
public static void deleteInvalidSessions(KeycloakSessionFactory sessionFactory, RealmModel realm, int batchSize) {
|
||||
long start = System.nanoTime();
|
||||
final String realmId = realm.getId();
|
||||
final String realmName = realm.getName();
|
||||
|
||||
logger.tracef("Removing invalid user sessions for realm '%s'", realmName);
|
||||
|
||||
int count = runJobInTransactionWithResult(sessionFactory, session -> {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
return em.createNamedQuery("deleteInvalidSessions")
|
||||
.setParameter("realmId", realmId)
|
||||
.executeUpdate();
|
||||
});
|
||||
List<UserSessionAndUser> invalidSession = new ArrayList<>();
|
||||
runInBatches(sessionFactory,
|
||||
s -> findAndRemoveSessions(s, realmId, batchSize, false, Details.INVALID_USER_SESSION_REMEMBER_ME_REASON, "findInvalidRegularUserSessions", NO_PARAMETERS, invalidSession),
|
||||
invalidSession::clear);
|
||||
|
||||
long duration = System.nanoTime() - start;
|
||||
logger.debugf("%d invalid session removed for realm '%s'. Took %dms", (Object) count, realmName, TimeUnit.NANOSECONDS.toMillis(duration));
|
||||
logger.debugf("Invalid session removed for realm '%s'. Took %dms", realmName, TimeUnit.NANOSECONDS.toMillis(duration));
|
||||
}
|
||||
|
||||
private static boolean handleRememberMeColumnValue(KeycloakSession session, String realmId, String realmName, int batchSize, boolean rememberMeEnabled, String queryName, Consumer<TypedQuery<Object[]>> setParameters, List<String> sessionsWithRememberMeCollector, List<String> sessionsWithoutRememberMeCollector) {
|
||||
private static boolean handleRememberMeColumnValue(KeycloakSession session, String realmId, String realmName, int batchSize, boolean rememberMeEnabled, String queryName, Consumer<TypedQuery<Object[]>> setParameters, List<UserSessionAndUser> sessionsWithRememberMeCollector, List<String> sessionsWithoutRememberMeCollector) {
|
||||
final EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
TypedQuery<Object[]> query = em.createNamedQuery(queryName, Object[].class);
|
||||
@ -224,35 +214,41 @@ final class UserSessionExpirationLogic {
|
||||
.setMaxResults(batchSize)
|
||||
.getResultStream()
|
||||
.map(UserSessionIdAndRememberMe::fromQueryProjection)
|
||||
.forEach(userSession -> (userSession.rememberMe() ? sessionsWithRememberMeCollector : sessionsWithoutRememberMeCollector).add(userSession.id()));
|
||||
.forEach(userSession -> {
|
||||
if (userSession.rememberMe()) {
|
||||
sessionsWithRememberMeCollector.add(userSession.sessionAndUser());
|
||||
} else {
|
||||
sessionsWithoutRememberMeCollector.add(userSession.sessionAndUser().userSessionId());
|
||||
}
|
||||
});
|
||||
|
||||
int updateCount = updateRememberMeColumn(em, false, sessionsWithoutRememberMeCollector);
|
||||
if (rememberMeEnabled) {
|
||||
int rememberMeUpdateCount = updateRememberMeColumn(em, true, sessionsWithRememberMeCollector);
|
||||
int rememberMeUpdateCount = updateRememberMeColumn(em, true, sessionsWithRememberMeCollector.stream().map(UserSessionAndUser::userSessionId).toList());
|
||||
logger.debugf("%d sessions with remember me, and %d sessions without remember updated, for realm '%s'", rememberMeUpdateCount, updateCount, realmName);
|
||||
} else {
|
||||
int deletedCount = deleteUserSessions(em, offlineToString(false), sessionsWithRememberMeCollector);
|
||||
int deletedCount = handleResultsToRemove(session, em, realmId, false, Details.INVALID_USER_SESSION_REMEMBER_ME_REASON, sessionsWithRememberMeCollector);
|
||||
logger.debugf("%d sessions without remember me updated, and %d invalid sessions deleted, for realm '%s'", updateCount, deletedCount, realmName);
|
||||
}
|
||||
|
||||
return sessionsWithRememberMeCollector.size() + sessionsWithoutRememberMeCollector.size() >= batchSize;
|
||||
}
|
||||
|
||||
private static void createExpirationEvent(KeycloakSession session, RealmModel realm, UserSessionAndUser userSessionAndUser) {
|
||||
private static void createUserSessionDeletedEvent(KeycloakSession session, RealmModel realm, UserSessionAndUser data, String reason) {
|
||||
new EventBuilder(realm, session)
|
||||
.user(userSessionAndUser.userId())
|
||||
.session(userSessionAndUser.userSessionId())
|
||||
.user(data.userId())
|
||||
.session(data.userSessionId())
|
||||
.event(EventType.USER_SESSION_DELETED)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.detail(Details.REASON, reason)
|
||||
.success();
|
||||
}
|
||||
|
||||
// returns true if it has more rows to check
|
||||
private static boolean removeExpiredOfflineSessionsInTransaction(KeycloakSession session, String realmId, int batchSize, String queryName, Consumer<TypedQuery<Object[]>> setParameters, List<UserSessionAndUser> expiredSessions) {
|
||||
private static boolean findAndRemoveSessions(KeycloakSession session, String realmId, int batchSize, boolean offline, String eventReason, String queryName, Consumer<TypedQuery<Object[]>> queryParameters, List<UserSessionAndUser> expiredSessions) {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
TypedQuery<Object[]> query = em.createNamedQuery(queryName, Object[].class);
|
||||
setParameters.accept(query);
|
||||
queryParameters.accept(query);
|
||||
query.setParameter("realmId", realmId)
|
||||
.setHint(AvailableHints.HINT_READ_ONLY, true)
|
||||
.setMaxResults(batchSize)
|
||||
@ -260,37 +256,16 @@ final class UserSessionExpirationLogic {
|
||||
.map(UserSessionAndUser::fromQueryProjection)
|
||||
.forEach(expiredSessions::add);
|
||||
|
||||
handleExpirationQueryResults(session, em, realmId, expiredSessions, true);
|
||||
handleResultsToRemove(session, em, realmId, offline, eventReason, expiredSessions);
|
||||
|
||||
// This should be safe.
|
||||
// If the hits are less than the desired batch size, we should not have expired sessions.
|
||||
return expiredSessions.size() >= batchSize;
|
||||
}
|
||||
|
||||
// returns true if it has more rows to check
|
||||
private static boolean removeExpiredRegularSessionInTransaction(KeycloakSession session, String realmId, boolean rememberMe, int batchSize, String queryName, Consumer<TypedQuery<Object[]>> setParameters, List<UserSessionAndUser> expiredSessions) {
|
||||
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
|
||||
|
||||
TypedQuery<Object[]> query = em.createNamedQuery(queryName, Object[].class);
|
||||
setParameters.accept(query);
|
||||
query.setParameter("realmId", realmId)
|
||||
.setParameter("rememberMe", rememberMe)
|
||||
.setHint(AvailableHints.HINT_READ_ONLY, true)
|
||||
.setMaxResults(batchSize)
|
||||
.getResultStream()
|
||||
.map(UserSessionAndUser::fromQueryProjection)
|
||||
.forEach(expiredSessions::add);
|
||||
|
||||
handleExpirationQueryResults(session, em, realmId, expiredSessions, false);
|
||||
|
||||
// This should be safe.
|
||||
// If the hits are less than the desired batch size, we should not have expired sessions.
|
||||
return expiredSessions.size() >= batchSize;
|
||||
}
|
||||
|
||||
private static void handleExpirationQueryResults(KeycloakSession session, EntityManager em, String realmId, Collection<UserSessionAndUser> expiredSessions, boolean offline) {
|
||||
private static int handleResultsToRemove(KeycloakSession session, EntityManager em, String realmId, boolean offline, String eventReason, Collection<UserSessionAndUser> expiredSessions) {
|
||||
if (expiredSessions.isEmpty()) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
@ -298,7 +273,7 @@ final class UserSessionExpirationLogic {
|
||||
|
||||
// creates the expiration events and extracts the user session IDs for the delete statement.
|
||||
var sessionIds = expiredSessions.stream()
|
||||
.peek(sessionAndUser -> createExpirationEvent(session, realm, sessionAndUser))
|
||||
.peek(sessionAndUser -> createUserSessionDeletedEvent(session, realm, sessionAndUser, eventReason))
|
||||
.map(UserSessionAndUser::userSessionId)
|
||||
.toList();
|
||||
|
||||
@ -309,8 +284,12 @@ final class UserSessionExpirationLogic {
|
||||
.setParameter("offline", offlineStr)
|
||||
.executeUpdate();
|
||||
|
||||
int us = deleteUserSessions(em, offlineStr, sessionIds);
|
||||
logger.debugf("Removed %d expired user sessions and %d expired client sessions in realm '%s'", us, cs, realm.getName());
|
||||
int us = em.createNamedQuery("deleteUserSessions")
|
||||
.setParameter("offline", offlineStr)
|
||||
.setParameter("userSessionIds", sessionIds)
|
||||
.executeUpdate();
|
||||
logger.debugf("Removed %d user sessions and %d client sessions in realm '%s'", us, cs, realm.getName());
|
||||
return us;
|
||||
}
|
||||
|
||||
private static int updateRememberMeColumn(EntityManager em, boolean rememberMe, Collection<String> sessionIds) {
|
||||
@ -323,14 +302,12 @@ final class UserSessionExpirationLogic {
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
private static int deleteUserSessions(EntityManager em, String offlineStr, Collection<String> sessionIds) {
|
||||
if (sessionIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return em.createNamedQuery("deleteUserSessions")
|
||||
.setParameter("offline", offlineStr)
|
||||
.setParameter("userSessionIds", sessionIds)
|
||||
.executeUpdate();
|
||||
private static void runInBatches(KeycloakSessionFactory sessionFactory, KeycloakSessionTaskWithResult<Boolean> task, Runnable afterBatchAction) {
|
||||
boolean hasMore;
|
||||
do {
|
||||
hasMore = runJobInTransactionWithResult(sessionFactory, null, task, "session-expiration-task");
|
||||
afterBatchAction.run();
|
||||
} while (hasMore);
|
||||
}
|
||||
|
||||
private static <T> Consumer<TypedQuery<T>> setLastSessionRefresh(int value) {
|
||||
@ -340,4 +317,8 @@ final class UserSessionExpirationLogic {
|
||||
private static <T> Consumer<TypedQuery<T>> setCreatedOn(int value) {
|
||||
return query -> query.setParameter("createdOn", value);
|
||||
}
|
||||
|
||||
private static <T> Consumer<TypedQuery<T>> setRememberMe(boolean value) {
|
||||
return query -> query.setParameter("rememberMe", value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,23 +23,25 @@ import java.util.Objects;
|
||||
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
record UserSessionIdAndRememberMe(String id, boolean rememberMe) {
|
||||
record UserSessionIdAndRememberMe(UserSessionAndUser sessionAndUser, boolean rememberMe) {
|
||||
|
||||
UserSessionIdAndRememberMe {
|
||||
Objects.requireNonNull(id);
|
||||
Objects.requireNonNull(sessionAndUser);
|
||||
}
|
||||
|
||||
static UserSessionIdAndRememberMe fromQueryProjection(Object[] projection) {
|
||||
assert projection.length == 2;
|
||||
assert projection.length == 3;
|
||||
assert projection[0] != null;
|
||||
assert projection[1] != null;
|
||||
assert projection[2] != null;
|
||||
try {
|
||||
String id = String.valueOf(projection[0]);
|
||||
String data = String.valueOf(projection[1]);
|
||||
String sessionId = String.valueOf(projection[0]);
|
||||
String userId = String.valueOf(projection[1]);
|
||||
String data = String.valueOf(projection[2]);
|
||||
Map<?, ?> values = JsonSerialization.readValue(data, Map.class);
|
||||
// TODO should we make PersistentUserSessionData public?
|
||||
boolean rememberMe = Boolean.parseBoolean(String.valueOf(values.get("rememberMe")));
|
||||
return new UserSessionIdAndRememberMe(id, rememberMe);
|
||||
return new UserSessionIdAndRememberMe(new UserSessionAndUser(sessionId, userId), rememberMe);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@ -117,5 +117,6 @@ public interface Details {
|
||||
String CLIENT_POLICY_ERROR = "client_policy_error";
|
||||
String CLIENT_POLICY_ERROR_DETAIL = "client_policy_error_detail";
|
||||
|
||||
String EXPIRED_DETAIL = "expired";
|
||||
String USER_SESSION_EXPIRED_REASON = "user_session_expired";
|
||||
String INVALID_USER_SESSION_REMEMBER_ME_REASON = "invalid_user_session_remember_me";
|
||||
}
|
||||
|
||||
@ -195,7 +195,7 @@ public class AssertEvents implements TestRule {
|
||||
return expect(EventType.USER_SESSION_DELETED)
|
||||
.session(sessionId)
|
||||
.user(userId)
|
||||
.detail(Details.REASON, Details.EXPIRED_DETAIL)
|
||||
.detail(Details.REASON, Details.USER_SESSION_EXPIRED_REASON)
|
||||
.client((String) null)
|
||||
.ipAddress((String) null);
|
||||
}
|
||||
@ -387,7 +387,7 @@ public class AssertEvents implements TestRule {
|
||||
if (key.equals(Details.SCOPE)) {
|
||||
// the scopes can be given in any order,
|
||||
// therefore, use a matcher that takes a string and ignores the order of the scopes
|
||||
return detail(key, new TypeSafeMatcher<String>() {
|
||||
return detail(key, new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(String actualValue) {
|
||||
return Matchers.containsInAnyOrder(value.split(" ")).matches(Arrays.asList(actualValue.split(" ")));
|
||||
@ -405,7 +405,7 @@ public class AssertEvents implements TestRule {
|
||||
|
||||
public ExpectedEvent detail(String key, Matcher<? super String> matcher) {
|
||||
if (details == null) {
|
||||
details = new HashMap<String, Matcher<? super String>>();
|
||||
details = new HashMap<>();
|
||||
}
|
||||
details.put(key, matcher);
|
||||
return this;
|
||||
@ -530,7 +530,7 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public static Matcher<String> isUUID() {
|
||||
return new TypeSafeMatcher<String>() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(String item) {
|
||||
return 36 == item.length() && item.charAt(8) == '-' && item.charAt(13) == '-' && item.charAt(18) == '-' && item.charAt(23) == '-';
|
||||
@ -544,7 +544,7 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public static Matcher<String> isAccessTokenId(String expectedGrantShortcut) {
|
||||
return new TypeSafeMatcher<String>() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(String item) {
|
||||
String[] items = item.split(":");
|
||||
@ -562,7 +562,7 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public Matcher<String> defaultRealmId() {
|
||||
return new TypeSafeMatcher<String>() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
private String realmId;
|
||||
|
||||
@Override
|
||||
@ -590,7 +590,7 @@ public class AssertEvents implements TestRule {
|
||||
}
|
||||
|
||||
public Matcher<String> defaultUserId() {
|
||||
return new TypeSafeMatcher<String>() {
|
||||
return new TypeSafeMatcher<>() {
|
||||
private String userId;
|
||||
|
||||
@Override
|
||||
|
||||
@ -148,7 +148,7 @@ public class UserSessionExpirationTest extends KeycloakModelTest {
|
||||
return provider.createQuery()
|
||||
.type(EventType.USER_SESSION_DELETED)
|
||||
.getResultStream()
|
||||
.filter(event -> Details.EXPIRED_DETAIL.equals(event.getDetails().get(Details.REASON)))
|
||||
.filter(event -> Details.USER_SESSION_EXPIRED_REASON.equals(event.getDetails().get(Details.REASON)))
|
||||
.count();
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ public class UserSessionExpirationTest extends KeycloakModelTest {
|
||||
return provider.createQuery()
|
||||
.type(EventType.USER_SESSION_DELETED)
|
||||
.getResultStream()
|
||||
.filter(event -> Details.EXPIRED_DETAIL.equals(event.getDetails().get(Details.REASON)))
|
||||
.filter(event -> Details.USER_SESSION_EXPIRED_REASON.equals(event.getDetails().get(Details.REASON)))
|
||||
.collect(Collectors.toUnmodifiableMap(Event::getSessionId, Event::getUserId));
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.BrokenBarrierException;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
@ -46,6 +47,8 @@ import org.keycloak.common.util.MultiSiteUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||
@ -929,6 +932,13 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
return expiration;
|
||||
});
|
||||
|
||||
String userId = withRealm(realmId, (session, realm) -> {
|
||||
// enable events
|
||||
realm.setEventsEnabled(true);
|
||||
realm.setEnabledEventTypes(Set.of(EventType.USER_SESSION_DELETED.name()));
|
||||
return session.users().getUserByUsername(realm, "user1").getId();
|
||||
});
|
||||
|
||||
final int initialCount = getPersistedUserSessionsCount();
|
||||
final int sessionCount = JpaUserSessionPersisterProviderFactory.DEFAULT_EXPIRATION_BATCH * 2;
|
||||
|
||||
@ -950,6 +960,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
// realm has remember me disabled, so half of the session should be deleted by the expiration job.
|
||||
triggerExpiration(realmExpiration.maxIdle() + 10);
|
||||
assertEquals(initialCount + (sessionCount / 2), getPersistedUserSessionsCount());
|
||||
assertEquals((sessionCount / 2), getUserSessionInvalidRememberMeEventCount(userId));
|
||||
|
||||
// check if everything worked as expected.
|
||||
withRealmConsumer(realmId, (session, realm) -> {
|
||||
@ -970,9 +981,13 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
createSessions(sessionCount, value -> value % 2 == 0);
|
||||
assertEquals(initialCount + sessionCount, getPersistedUserSessionsCount());
|
||||
|
||||
// clear events
|
||||
withRealmConsumer(realmId, (session, realm) -> session.getProvider(EventStoreProvider.class).clear(realm));
|
||||
|
||||
// realm has remember me disabled, so half of the session should be deleted by the expiration job.
|
||||
triggerExpiration(realmExpiration.maxIdle() + 10);
|
||||
assertEquals(initialCount + (sessionCount / 2), getPersistedUserSessionsCount());
|
||||
assertEquals(sessionCount / 2, getUserSessionInvalidRememberMeEventCount(userId));
|
||||
|
||||
triggerExpiration(realmExpiration.maxIdle() + PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS + 10);
|
||||
assertEquals(initialCount, getPersistedUserSessionsCount());
|
||||
@ -1212,6 +1227,14 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
}
|
||||
|
||||
private long getUserSessionExpirationEventCount(String userId) {
|
||||
return getUserSessionDeletedEventCount(userId, Details.USER_SESSION_EXPIRED_REASON);
|
||||
}
|
||||
|
||||
private long getUserSessionInvalidRememberMeEventCount(String userId) {
|
||||
return getUserSessionDeletedEventCount(userId, Details.INVALID_USER_SESSION_REMEMBER_ME_REASON);
|
||||
}
|
||||
|
||||
private long getUserSessionDeletedEventCount(String userId, String reason) {
|
||||
return withRealm(realmId, (session, ignored) -> {
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
return eventStore.createQuery()
|
||||
@ -1219,10 +1242,18 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
.user(userId)
|
||||
.type(EventType.USER_SESSION_DELETED)
|
||||
.getResultStream()
|
||||
.map(UserSessionPersisterProviderTest::findReason)
|
||||
.flatMap(Optional::stream)
|
||||
.filter(reason::equals)
|
||||
.count();
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<String> findReason(Event event) {
|
||||
return Optional.ofNullable(event.getDetails())
|
||||
.map(map -> map.get(Details.REASON));
|
||||
}
|
||||
|
||||
private long countUserSessionsInRealm(KeycloakSession session) {
|
||||
JpaUserSessionPersisterProvider sessionPersisterProvider = (JpaUserSessionPersisterProvider) session.getProvider(UserSessionPersisterProvider.class);
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user