diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java index be69145b17..a9d85bc1b6 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits/UserSessionLimitsAuthenticator.java @@ -71,12 +71,12 @@ public class UserSessionLimitsAuthenticator implements Authenticator { if (exceedsLimit(userSessionCountForRealm, userRealmLimit)) { logger.infof("Too many session in this realm for the current user. Session count: %s", userSessionCountForRealm); String eventDetails = String.format(realmEventDetailsTemplate, context.getRealm().getName(), userRealmLimit, userSessionCountForRealm, context.getUser().getId()); - handleLimitExceeded(context, userSessionsForRealm, eventDetails); + handleLimitExceeded(context, userSessionsForRealm, eventDetails, userRealmLimit); } // otherwise if the user is still allowed to create a new session in the realm, check if this applies for this specific client as well. else if (exceedsLimit(userSessionCountForClient, userClientLimit)) { logger.infof("Too many sessions related to the current client for this user. Session count: %s", userSessionCountForRealm); String eventDetails = String.format(clientEventDetailsTemplate, context.getRealm().getName(), userClientLimit, userSessionCountForClient, context.getUser().getId()); - handleLimitExceeded(context, userSessionsForClient, eventDetails); + handleLimitExceeded(context, userSessionsForClient, eventDetails, userClientLimit); } else { context.success(); } @@ -89,7 +89,11 @@ public class UserSessionLimitsAuthenticator implements Authenticator { if (limit <= 0) { // if limit is zero or negative, consider the limit disabled return false; } - return count > limit - 1; + return getNumberOfSessionsThatNeedToBeLoggedOut(count, limit) > 0; + } + + private long getNumberOfSessionsThatNeedToBeLoggedOut(long count, long limit) { + return count - (limit - 1); } private int getIntConfigProperty(String key, Map config) { @@ -135,7 +139,7 @@ public class UserSessionLimitsAuthenticator implements Authenticator { } - private void handleLimitExceeded(AuthenticationFlowContext context, List userSessions, String eventDetails) { + private void handleLimitExceeded(AuthenticationFlowContext context, List userSessions, String eventDetails, long limit) { switch (behavior) { case UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION: logger.info("Denying new session"); @@ -158,15 +162,24 @@ public class UserSessionLimitsAuthenticator implements Authenticator { case UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION: logger.info("Terminating oldest session"); - logoutOldestSession(userSessions); + logoutOldestSessions(userSessions, limit); context.success(); break; } } - private void logoutOldestSession(List userSessions) { - logger.info("Logging out oldest session"); - Optional oldest = userSessions.stream().sorted(Comparator.comparingInt(UserSessionModel::getLastSessionRefresh)).findFirst(); - oldest.ifPresent(userSession -> AuthenticationManager.backchannelLogout(session, userSession, true)); + private void logoutOldestSessions(List userSessions, long limit) { + long numberOfSessionsThatNeedToBeLoggedOut = getNumberOfSessionsThatNeedToBeLoggedOut(userSessions.size(), limit); + if (numberOfSessionsThatNeedToBeLoggedOut == 1) { + logger.info("Logging out oldest session"); + } else { + logger.infof("Logging out oldest %s sessions", numberOfSessionsThatNeedToBeLoggedOut); + } + + userSessions + .stream() + .sorted(Comparator.comparingInt(UserSessionModel::getLastSessionRefresh)) + .limit(numberOfSessionsThatNeedToBeLoggedOut) + .forEach(userSession -> AuthenticationManager.backchannelLogout(session, userSession, true)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java index 9333241072..49b091d0b1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/sessionlimits/UserSessionLimitsTest.java @@ -212,14 +212,21 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest { public void testClientSessionCountExceededAndOldestSessionRemovedDirectGrantFlow() throws Exception { try { setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION); - OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); - assertEquals(200, response.getStatusCode()); + setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_CLIENT_LIMIT, "2"); + for (int i = 0; i < 2; ++i) { + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + assertEquals(200, response.getStatusCode()); + } + testingClient.server(realmName).run(assertSessionCount(realmName, username, 2)); - response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_CLIENT_LIMIT, "1"); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); assertEquals(200, response.getStatusCode()); testingClient.server(realmName).run(assertSessionCount(realmName, username, 1)); } finally { setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION); + setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_REALM_LIMIT, "0"); + setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.USER_CLIENT_LIMIT, "1"); } }