Terminating all sessions above the session limit (#16068)

Adjusts implementation of UserSessionLimitsAuthenticator to terminate all sessions above the session limit.

Closes #14689

Co-authored-by: Marek Posolda <mposolda@gmail.com>
This commit is contained in:
drohwer89 2023-02-16 17:56:59 +01:00 committed by GitHub
parent 9995a3cdd4
commit 4ff180da64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 32 additions and 12 deletions

View file

@ -71,12 +71,12 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
if (exceedsLimit(userSessionCountForRealm, userRealmLimit)) { if (exceedsLimit(userSessionCountForRealm, userRealmLimit)) {
logger.infof("Too many session in this realm for the current user. Session count: %s", userSessionCountForRealm); 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()); 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. } // 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)) { else if (exceedsLimit(userSessionCountForClient, userClientLimit)) {
logger.infof("Too many sessions related to the current client for this user. Session count: %s", userSessionCountForRealm); 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()); String eventDetails = String.format(clientEventDetailsTemplate, context.getRealm().getName(), userClientLimit, userSessionCountForClient, context.getUser().getId());
handleLimitExceeded(context, userSessionsForClient, eventDetails); handleLimitExceeded(context, userSessionsForClient, eventDetails, userClientLimit);
} else { } else {
context.success(); context.success();
} }
@ -89,7 +89,11 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
if (limit <= 0) { // if limit is zero or negative, consider the limit disabled if (limit <= 0) { // if limit is zero or negative, consider the limit disabled
return false; 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<String, String> config) { private int getIntConfigProperty(String key, Map<String, String> config) {
@ -135,7 +139,7 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
} }
private void handleLimitExceeded(AuthenticationFlowContext context, List<UserSessionModel> userSessions, String eventDetails) { private void handleLimitExceeded(AuthenticationFlowContext context, List<UserSessionModel> userSessions, String eventDetails, long limit) {
switch (behavior) { switch (behavior) {
case UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION: case UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION:
logger.info("Denying new session"); logger.info("Denying new session");
@ -158,15 +162,24 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
case UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION: case UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION:
logger.info("Terminating oldest session"); logger.info("Terminating oldest session");
logoutOldestSession(userSessions); logoutOldestSessions(userSessions, limit);
context.success(); context.success();
break; break;
} }
} }
private void logoutOldestSession(List<UserSessionModel> userSessions) { private void logoutOldestSessions(List<UserSessionModel> userSessions, long limit) {
long numberOfSessionsThatNeedToBeLoggedOut = getNumberOfSessionsThatNeedToBeLoggedOut(userSessions.size(), limit);
if (numberOfSessionsThatNeedToBeLoggedOut == 1) {
logger.info("Logging out oldest session"); logger.info("Logging out oldest session");
Optional<UserSessionModel> oldest = userSessions.stream().sorted(Comparator.comparingInt(UserSessionModel::getLastSessionRefresh)).findFirst(); } else {
oldest.ifPresent(userSession -> AuthenticationManager.backchannelLogout(session, userSession, true)); logger.infof("Logging out oldest %s sessions", numberOfSessionsThatNeedToBeLoggedOut);
}
userSessions
.stream()
.sorted(Comparator.comparingInt(UserSessionModel::getLastSessionRefresh))
.limit(numberOfSessionsThatNeedToBeLoggedOut)
.forEach(userSession -> AuthenticationManager.backchannelLogout(session, userSession, true));
} }
} }

View file

@ -212,14 +212,21 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest {
public void testClientSessionCountExceededAndOldestSessionRemovedDirectGrantFlow() throws Exception { public void testClientSessionCountExceededAndOldestSessionRemovedDirectGrantFlow() throws Exception {
try { try {
setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION); setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION);
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"); OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
assertEquals(200, response.getStatusCode()); 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()); assertEquals(200, response.getStatusCode());
testingClient.server(realmName).run(assertSessionCount(realmName, username, 1)); testingClient.server(realmName).run(assertSessionCount(realmName, username, 1));
} finally { } finally {
setAuthenticatorConfigItem(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION); 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");
} }
} }