Validate client session timeout and lifetime settings on realm settings edit

Closes #44910

Signed-off-by: Ruchika <Ruchika.Jha1@ibm.com>
Signed-off-by: Ryan Emerson <remerson@ibm.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: Ryan Emerson <remerson@ibm.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
Ruchika Jha 2026-01-05 08:50:56 +00:00 committed by GitHub
parent adeb41e82b
commit 60b369c622
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 50 deletions

View File

@ -12,14 +12,21 @@ Setups on Windows that previously relied on custom machine names or non-standard
=== Validation of client session timeouts
Previous versions did not validate client specific settings against the realm settings for SSO session idle and max lifetime, including the remember me settings of the realm.
Previous versions did not validate client settings against the realm settings for SSO session idle and max lifetime, including the remember me settings of the realm.
This leads to those settings either not being effective with unexpected results to administrators,
or to refresh tokens issued where the user session might have already expired when the client was trying to refresh the token.
{project_name} now validates that a client specific settings for Client Session Idle and Client Session Max does not exceed the realm settings when a client is created or updated, and will show a validation error when the validation fails.
It does currently not validate the client settings when the realm SSO settings or remember me changes, though this might change in future releases.
{project_name} now validates when creating or updating realms that on the realm level the Client session settings do not exceed the SSO session settings.
You are only affected by the change if you have configured a client-specific Client Session Idle and Client Session Max setting in the Advanced tab of the client configuration that exceeds the realm settings.
It also validates that a client specific settings for Client Session Idle and Client Session Max does not exceed the realm settings when a client is created or updated, and will show a validation error when the validation fails.
It does currently not validate an individual's client settings when the realm SSO settings or remember me changes, though this might change in future releases.
You are only affected by the change:
* if you have configured a client-specific Client Session Idle and Client Session Max setting in the Advanced tab of the client configuration that exceeds the realm settings, or
* if you configured on the realm level client session settings that exceed the realm session settings.
If you are affected by the change, you will see error messages the next time you update or import an affected client or realm.
// ------------------------ Notable changes ------------------------ //
== Notable changes

View File

@ -900,6 +900,7 @@ public class DefaultExportImportManager implements ExportImportManager {
updateCibaSettings(rep, realm);
updateParSettings(rep, realm);
validateClientAndRealmTimeouts(realm);
session.clientPolicy().updateRealmModelFromRepresentation(realm, rep);
if (rep.getSmtpServer() != null) {
@ -1733,4 +1734,24 @@ public class DefaultExportImportManager implements ExportImportManager {
}
}
}
private void validateClientAndRealmTimeouts(RealmModel realm) {
if (realm.isRememberMe()) {
if (realm.getClientSessionIdleTimeout() > Math.max(realm.getSsoSessionIdleTimeout(), realm.getSsoSessionIdleTimeoutRememberMe())) {
throw new ModelException("Client session idle timeout cannot exceed realm SSO session idle timeout and RememberMe idle timeout.");
}
if (realm.getClientSessionMaxLifespan() > Math.max(realm.getSsoSessionMaxLifespan(), realm.getSsoSessionMaxLifespanRememberMe())) {
throw new ModelException("Client session max lifespan cannot exceed realm SSO session max lifespan and RememberMe Max span.");
}
} else {
if (realm.getClientSessionIdleTimeout() > realm.getSsoSessionIdleTimeout()) {
throw new ModelException("Client Session Idle Timeout cannot be greater than Realm SSO Idle Timeout.");
}
if (realm.getClientSessionMaxLifespan() > realm.getSsoSessionMaxLifespan()) {
throw new ModelException("Client session max lifespan cannot exceed realm SSO session max lifespan.");
}
}
}
}

View File

@ -288,4 +288,63 @@ public class RealmUpdateTest extends AbstractRealmTest {
assertEquals(expected.isAdminEventsEnabled(), actual.isAdminEventsEnabled());
assertEquals(expected.isAdminEventsDetailsEnabled(), actual.isAdminEventsDetailsEnabled());
}
@Test
public void testRealmClientSessionTimeoutValidation() {
RealmRepresentation rep = managedRealm.admin().toRepresentation();
// Remember-Me Disabled
rep.setRememberMe(false);
rep.setSsoSessionIdleTimeout(300);
rep.setSsoSessionMaxLifespan(600);
// Invalid: client idle > realm idle
rep.setClientSessionIdleTimeout(400);
rep.setClientSessionMaxLifespan(500);
try {
managedRealm.admin().update(rep);
Assertions.fail("Expected validation error for client idle timeout");
} catch (Exception e) {
assertEquals("HTTP 400 Bad Request", e.getMessage());
}
// Fix idle, break max lifespan
rep.setClientSessionIdleTimeout(200);
rep.setClientSessionMaxLifespan(700);
try {
managedRealm.admin().update(rep);
Assertions.fail("Expected validation error for client max lifespan");
} catch (Exception e) {
assertEquals("HTTP 400 Bad Request", e.getMessage());
}
// Remember-Me Enabled
rep = managedRealm.admin().toRepresentation();
rep.setRememberMe(true);
rep.setSsoSessionIdleTimeout(300);
rep.setSsoSessionIdleTimeoutRememberMe(500);
rep.setSsoSessionMaxLifespan(600);
rep.setSsoSessionMaxLifespanRememberMe(900);
// Invalid: exceeds allowed remember-me idle
rep.setClientSessionIdleTimeout(550);
rep.setClientSessionMaxLifespan(800);
try {
managedRealm.admin().update(rep);
Assertions.fail("Expected validation error for remember-me idle timeout");
} catch (Exception e) {
assertEquals("HTTP 400 Bad Request", e.getMessage());
}
// Fix idle, break max lifespan
rep.setClientSessionIdleTimeout(300);
rep.setClientSessionMaxLifespan(950);
try {
managedRealm.admin().update(rep);
Assertions.fail("Expected validation error for remember-me max lifespan");
} catch (Exception e) {
assertEquals("HTTP 400 Bad Request", e.getMessage());
}
}
}

View File

@ -1464,52 +1464,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
}
}
@Test
public void refreshTokenUserClientMaxLifespanGreaterThanSession() throws Exception {
RealmResource realmResource = adminClient.realm("test");
getTestingClient().testing().setTestingInfinispanTimeService();
try (Closeable ignored = new RealmAttributeUpdater(realmResource)
.updateWith(r -> {
r.setSsoSessionMaxLifespan(3600);
r.setSsoSessionIdleTimeout(7200);
r.setClientSessionMaxLifespan(5000);
r.setClientSessionIdleTimeout(7200);
}).update()) {
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600);
String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
events.poll();
setTimeOffset(1800);
String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800);
assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
events.expectRefresh(refreshId, sessionId).assertEvent();
setTimeOffset(3700);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
assertEquals(400, tokenResponse.getStatusCode());
assertNull(tokenResponse.getAccessToken());
assertNull(tokenResponse.getRefreshToken());
events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
} finally {
getTestingClient().testing().revertTestingInfinispanTimeService();
events.clear();
resetTimeOffset();
}
}
@Test
public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
RealmResource realmResource = adminClient.realm("test");
@ -1537,6 +1491,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
RealmRepresentation rep = realmResource.toRepresentation();
rep.setSsoSessionMaxLifespan(3600);
rep.setClientSessionMaxLifespan(3600);
realmResource.update(rep);
setTimeOffset(3700);