diff --git a/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc index 8a0f9244bcd..ce8587732ca 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc @@ -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 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 b22811fc054..e4b3dc13040 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 @@ -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 logoutOldestSessions(List userSessions, long limit) { + private List logoutOldestSessions(List 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; 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 ba4618ea56f..23b3558f292 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 @@ -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);