Add logout event to UserSessionLimitsAuthenticator

Closes #44843

Signed-off-by: Robin Meese <39960884+robson90@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
Robin Meese 2026-01-01 14:22:54 +01:00 committed by GitHub
parent ac557234a2
commit 35ee49b5d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 37 additions and 7 deletions

View File

@ -39,11 +39,13 @@ only the basic attributes in representations or all of them.
The `UserProfile` interface is a private API and should not be implemented by custom code. However, if you have extensions that
implement this interface, you will need to update your code to accommodate this new method.
=== LOGOUT events when logging out sessions via the Account Console
=== Additional LOGOUT events
When logging out sessions via the Account Console {project_name} now creates LOGOUT user events to track this activity.
The events are connected to the `account` client.
When the number of sessions for a user is limited, and you have configured the oldest session to be logged out once the limit is reached, {project_name} now creates LOGOUT user events to track this. The events are connected to the client that triggered the new login.
=== Identity Provider refactoring
The private SPI for identity providers has been refactored. This is to allow identity providers to support more use

View File

@ -14,6 +14,8 @@ import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.Authenticator;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@ -179,7 +181,7 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
case UserSessionLimitsAuthenticatorFactory.TERMINATE_OLDEST_SESSION:
logger.info("Terminating oldest session");
var removedSessions = logoutOldestSessions(userSessions, limit);
var removedSessions = logoutOldestSessions(userSessions, limit, context.getEvent());
context.success();
return removedSessions;
}
@ -190,7 +192,7 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
/**
* @return A list of logged-out user sessions, if any.
*/
private List<UserSessionModel> logoutOldestSessions(List<UserSessionModel> userSessions, long limit) {
private List<UserSessionModel> logoutOldestSessions(List<UserSessionModel> userSessions, long limit, EventBuilder eventBuilder) {
long numberOfSessionsThatNeedToBeLoggedOut = getNumberOfSessionsThatNeedToBeLoggedOut(userSessions.size(), limit);
if (numberOfSessionsThatNeedToBeLoggedOut == 1) {
logger.info("Logging out oldest session");
@ -206,6 +208,11 @@ public class UserSessionLimitsAuthenticator implements Authenticator {
for (UserSessionModel userSession : userSessionsToBeRemoved) {
AuthenticationManager.backchannelLogout(session, userSession, true);
eventBuilder.clone()
.event(EventType.LOGOUT)
.user(userSession.getUser())
.session(userSession.getId())
.success();
}
return userSessionsToBeRemoved;

View File

@ -46,6 +46,7 @@ import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
@ -171,14 +172,24 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest {
// Login and verify login was successful
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
EventRepresentation initialLoginEvent = events.expectLogin().assertEvent();
String userId = initialLoginEvent.getUserId();
String initialLoginSessionID = initialLoginEvent.getSessionId();
// Delete the cookies, while maintaining the server side session active
super.deleteCookies();
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
// assert we have a logout session event, as the authenticator should have deleted the first session.
events.expect(EventType.LOGOUT)
.user(userId)
.session(initialLoginSessionID)
.assertEvent();
// User is first logged out, then logged in with a fresh sessionId
events.expectLogin()
.session(Matchers.not(initialLoginSessionID))
.assertEvent();
testingClient.server(realmName).run(assertSessionCount(realmName, username, 1));
} finally {
setAuthenticatorConfigItem(DefaultAuthenticationFlows.BROWSER_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION);
@ -217,14 +228,24 @@ public class UserSessionLimitsTest extends AbstractTestRealmKeycloakTest {
setAuthenticatorConfigItem(DefaultAuthenticationFlows.BROWSER_FLOW, UserSessionLimitsAuthenticatorFactory.USER_CLIENT_LIMIT, "0");
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
EventRepresentation initialLoginEvent = events.expectLogin().assertEvent();
String userId = initialLoginEvent.getUserId();
String initialLoginSessionID = initialLoginEvent.getSessionId();
// Delete the cookies, while maintaining the server side session active
super.deleteCookies();
loginPage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().assertEvent();
// assert we have a logout session event, as the authenticator should have deleted the first session.
events.expect(EventType.LOGOUT)
.user(userId)
.session(initialLoginSessionID)
.assertEvent();
// User is first logged out, then logged in with a fresh sessionId
events.expectLogin()
.session(Matchers.not(initialLoginSessionID))
.assertEvent();
testingClient.server(realmName).run(assertSessionCount(realmName, username, 1));
} finally {
setAuthenticatorConfigItem(DefaultAuthenticationFlows.BROWSER_FLOW, UserSessionLimitsAuthenticatorFactory.BEHAVIOR, UserSessionLimitsAuthenticatorFactory.DENY_NEW_SESSION);