From bda0e2a67c8cf41d1b3d9010e6dfcddaf79bf59b Mon Sep 17 00:00:00 2001 From: Giuseppe Graziano Date: Tue, 14 Oct 2025 17:00:41 +0200 Subject: [PATCH] Invalidate sessions created with remember me when remember me is disabled for realm Closes #43328 Signed-off-by: Giuseppe Graziano --- .../topics/login-settings/remember-me.adoc | 2 + .../topics/changes/changes-26_5_0.adoc | 6 +++ .../admin/messages/messages_en.properties | 2 +- .../managers/AuthenticationManager.java | 4 ++ .../keycloak/testsuite/forms/LoginTest.java | 52 ++++++++++++++++--- .../session/SessionTimeoutValidationTest.java | 2 +- 6 files changed, 60 insertions(+), 8 deletions(-) diff --git a/docs/documentation/server_admin/topics/login-settings/remember-me.adoc b/docs/documentation/server_admin/topics/login-settings/remember-me.adoc index ce72ba53e99..0b2b598d35e 100644 --- a/docs/documentation/server_admin/topics/login-settings/remember-me.adoc +++ b/docs/documentation/server_admin/topics/login-settings/remember-me.adoc @@ -16,3 +16,5 @@ When you save this setting, a `remember me` checkbox displays on the realm's log .Remember Me image:images/remember-me.png[Remember Me] +WARNING: Note that disabling the "Remember me" option will invalidate all sessions created with the "Remember me" checkbox selected during login, requiring users to log in again. Any refresh tokens related to these sessions will also become invalid. +Note also that the sessions will not be invalidated immediately when the switch is disabled, but only when a cookie or token associated with an invalid session is used. This means that disabling and then re-enabling the "Remember me" switch cannot be used to invalidate old sessions. 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 325dd367963..3748627a61e 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_5_0.adoc @@ -12,6 +12,12 @@ The `log-console-color` previously defaulted to `false`, but it will now instead You may still explicitly disable color support by setting the option to `false`. +=== User sessions created with "Remember Me" are no longer valid if "Remember Me" is disabled for the realm + +When the "Remember Me" option is disabled in the realm settings, all user sessions previously created with the "Remember Me" flag are now considered invalid. +Users will be required to log in again, and any associated refresh tokens will no longer be usable. +User sessions created without selecting "Remember Me" are not affected. + // ------------------------ Deprecated features ------------------------ // == Deprecated features diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 320971b812a..0980bccb898 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -1821,7 +1821,7 @@ groupUpdateError=Error updating group {{error}} logoutAllSessions=Logout all sessions membershipUserLdapAttribute=Membership user LDAP attribute noKeysDescription=You haven't created any active keys -rememberMeHelpText=Show checkbox on the login page to allow the user to remain logged in between browser restarts until the session expires. +rememberMeHelpText=Show checkbox on the login page to allow the user to remain logged in between browser restarts until the session expires. If disabled, all sessions created with the "Remember me" checkbox selected during login are considered invalid. eventTypes.UPDATE_EMAIL.name=Update email notBeforeHelp=Revoke any tokens issued before this time for this client. To push the policy, you should set an effective admin URL in the Settings tab first. protocolTypes.saml=SAML diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 64cf1472503..95aef6c9625 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -185,6 +185,10 @@ public class AuthenticationManager { logger.debug("No user session"); return false; } + if (userSession.isRememberMe() && !realm.isRememberMe()) { + logger.debugv("Session {0} invalid: created with remember me but remember me is disabled for the realm.", userSession.getId()); + return false; + } if (userSession.getNote(Details.IDENTITY_PROVIDER) != null) { String brokerAlias = userSession.getNote(Details.IDENTITY_PROVIDER); if (realm.getIdentityProviderByAlias(brokerAlias) == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 5546c07ced7..bae12561c1b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -71,6 +71,7 @@ import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ContainerAssume; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.Matchers; +import org.keycloak.testsuite.util.oauth.AccessTokenResponse; import org.keycloak.testsuite.util.oauth.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.TokenSignatureUtil; @@ -86,6 +87,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.keycloak.common.Profile.Feature.DYNAMIC_SCOPES; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; @@ -170,7 +173,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { String headerValue = response.getHeaderString(header.getHeaderName()); String expectedValue = header.getDefaultValue(); if (expectedValue.isEmpty()) { - Assert.assertNull(headerValue); + assertNull(headerValue); } else { Assert.assertNotNull(headerValue); assertThat(headerValue, is(equalTo(expectedValue))); @@ -252,7 +255,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { Assert.assertEquals("", loginPage.getPassword()); Assert.assertEquals("Invalid username or password.", loginPage.getUsernameInputError()); - Assert.assertNull(loginPage.getPasswordInputError()); + assertNull(loginPage.getPasswordInputError()); events.expectLogin().user(user2Id).session((String) null).error("invalid_user_credentials") .detail(Details.USERNAME, "login-test2") @@ -279,7 +282,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { Assert.assertEquals("", loginPage.getPassword()); Assert.assertEquals("Invalid username or password.", loginPage.getUsernameInputError()); - Assert.assertNull(loginPage.getPasswordInputError()); + assertNull(loginPage.getPasswordInputError()); events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials") .detail(Details.USERNAME, "login-test") @@ -299,7 +302,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { Assert.assertEquals("", loginPage.getPassword()); Assert.assertEquals("Invalid username or password.", loginPage.getUsernameInputError()); - Assert.assertNull(loginPage.getPasswordInputError()); + assertNull(loginPage.getPasswordInputError()); events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials") .detail(Details.USERNAME, "login-test") @@ -683,7 +686,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { .detail(Details.USERNAME, "login-test") .assertEvent(); // check remember me is not set although it was sent in the form data - Assert.assertNull(loginEvent.getDetails().get(Details.REMEMBER_ME)); + assertNull(loginEvent.getDetails().get(Details.REMEMBER_ME)); } //KEYCLOAK-2741 @@ -769,6 +772,43 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { } } + @Test + public void testLoginAfterDisablingRememberMeInRealmSettings() { + setRememberMe(true); + + try { + //login with remember me + loginPage.open(); + loginPage.setRememberMe(true); + assertTrue(loginPage.isRememberMeChecked()); + loginPage.login("login@test.com", getPassword("login-test")); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.parseLoginResponse().getCode()); + events.expectLogin().user(userId) + .detail(Details.USERNAME, "login@test.com") + .detail(Details.REMEMBER_ME, "true") + .assertEvent(); + + AccessTokenResponse response = oauth.accessTokenRequest(oauth.parseLoginResponse().getCode()).send(); + + setRememberMe(false); + + //refresh fail + response = oauth.refreshRequest(response.getRefreshToken()).send(); + assertNull(response.getAccessToken()); + assertNotNull(response.getError()); + assertEquals("Session not active", response.getErrorDescription()); + + // Assert session removed + loginPage.open(); + assertFalse(loginPage.isRememberMeCheckboxPresent()); + assertNotEquals("login-test", loginPage.getUsername()); + } finally { + setRememberMe(false); + } + } + // Login timeout scenarios // KEYCLOAK-1037 @Test @@ -892,7 +932,7 @@ public class LoginTest extends AbstractChangeImportedUserPasswordsTest { oauth.openLoginForm(); loginPage.assertCurrent(); - Assert.assertNull("Not expected to have error on loginForm.", loginPage.getError()); + assertNull("Not expected to have error on loginForm.", loginPage.getError()); loginPage.login("test-user@localhost", getPassword("test-user@localhost")); appPage.assertCurrent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/SessionTimeoutValidationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/SessionTimeoutValidationTest.java index 3d900ee9660..9075e5e03d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/SessionTimeoutValidationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/session/SessionTimeoutValidationTest.java @@ -75,7 +75,7 @@ public class SessionTimeoutValidationTest extends AbstractTestRealmKeycloakTest session.sessions().createUserSession( null, realm, session.users().getUserByUsername(realm, "user1"), - "user1", "127.0.0.1", "form", true, null, null, + "user1", "127.0.0.1", "form", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); realm.setSsoSessionIdleTimeout(Integer.MAX_VALUE);