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)) {
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<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) {
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<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");
Optional<UserSessionModel> oldest = userSessions.stream().sorted(Comparator.comparingInt(UserSessionModel::getLastSessionRefresh)).findFirst();
oldest.ifPresent(userSession -> AuthenticationManager.backchannelLogout(session, userSession, true));
} 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));
}
}

View file

@ -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);
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");
}
}