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

Closes #43328


(cherry picked from commit bda0e2a67c8cf41d1b3d9010e6dfcddaf79bf59b)

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2025-10-15 21:08:05 +02:00 committed by GitHub
parent 89c960cd4e
commit a340941007
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 64 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

@ -0,0 +1,10 @@
// ------------------------ Notable changes ------------------------ //
== Notable changes
Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}.
=== 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.

View File

@ -1820,7 +1820,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);