Invalidate sessions created with remember me when remember me is disabled for realm

Closes #43328

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano
2025-10-14 17:00:41 +02:00
committed by GitHub
parent 38909da47d
commit bda0e2a67c
6 changed files with 60 additions and 8 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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();

View File

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