Added validation for client session timeout post comparing the realm session timeouts

Closes #41019

Signed-off-by: ruchikajha95 <Ruchika.Jha1@ibm.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
Ruchika Jha
2025-12-11 12:58:04 +00:00
committed by GitHub
parent 2feb158554
commit 26fe8dc7d8
5 changed files with 170 additions and 2 deletions

View File

@@ -1,12 +1,26 @@
// ------------------------ Breaking changes ------------------------ // // ------------------------ Breaking changes ------------------------ //
== Breaking changes == Breaking changes
= Loopback Hostname Verification on Windows Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.
=== Loopback Hostname Verification on Windows
Setups on Windows that previously relied on custom machine names or non-standard hostnames for loopback (for example, `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification. Setups on Windows that previously relied on custom machine names or non-standard hostnames for loopback (for example, `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification.
{project_name} now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS. {project_name} now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS.
=== 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.
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.
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.
// ------------------------ Notable changes ------------------------ // // ------------------------ Notable changes ------------------------ //
== Notable changes == Notable changes

View File

@@ -347,4 +347,10 @@ public class Messages {
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership"; public static final String CONFIRM_ORGANIZATION_MEMBERSHIP = "organization.confirm-membership";
public static final String CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE = "organization.confirm-membership.title"; public static final String CONFIRM_ORGANIZATION_MEMBERSHIP_TITLE = "organization.confirm-membership.title";
public static final String REGISTER_ORGANIZATION_MEMBER = "organization.member.register.title"; public static final String REGISTER_ORGANIZATION_MEMBER = "organization.member.register.title";
// Client sessions
public static final String CLIENT_IDLE_REMEMBERME = "clientIdleExceedsRealmRememberMeIdle";
public static final String CLIENT_IDLE = "clientSessionIdleTimeoutExceedsRealm";
public static final String CLIENT_MAXLIFE_SPAN = "clientSessionMaxLifespanExceedsRealm";
public static final String CLIENT_MAXLIFESPAN_REMEMBERME = "clientSessionMaxLifespanExceedsRealmRememberMeMaxSpan";
} }

View File

@@ -28,6 +28,7 @@ import java.util.Set;
import org.keycloak.authentication.authenticators.util.LoAUtil; import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapperConfigException; import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
@@ -41,6 +42,7 @@ import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
@@ -193,6 +195,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
validateJwks(context); validateJwks(context);
validateDefaultAcrValues(context); validateDefaultAcrValues(context);
validateMinimumAcrValue(context); validateMinimumAcrValue(context);
validateClientSessionTimeout(context);
return context.toResult(); return context.toResult();
} }
@@ -205,7 +208,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
new CibaClientValidation(context).validate(); new CibaClientValidation(context).validate();
validateDefaultAcrValues(context); validateDefaultAcrValues(context);
validateMinimumAcrValue(context); validateMinimumAcrValue(context);
//context.getSession().getContext().getRealm().
return context.toResult(); return context.toResult();
} }
@@ -219,6 +222,7 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
private void validateUrls(ValidationContext<ClientModel> context) { private void validateUrls(ValidationContext<ClientModel> context) {
ClientModel client = context.getObjectToValidate(); ClientModel client = context.getObjectToValidate();
// Use a fake URL for validating relative URLs as we may not be validating clients in the context of a request (import at startup) // Use a fake URL for validating relative URLs as we may not be validating clients in the context of a request (import at startup)
String authServerUrl = "https://localhost/auth"; String authServerUrl = "https://localhost/auth";
@@ -421,4 +425,71 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
} }
} }
} }
private void validateClientSessionTimeout(ValidationContext<ClientModel> context) {
ClientModel clientModel = context.getObjectToValidate();
if (clientModel == null ) return;
RealmModel realmModel = clientModel.getRealm();
if (realmModel == null ) return;
//Realm values
int realmIdle = realmModel.getSsoSessionIdleTimeout();
int realmMax = realmModel.getSsoSessionMaxLifespan();
int realmRememberIdle = realmModel.getSsoSessionIdleTimeoutRememberMe();
int realmRememberMax = realmModel.getSsoSessionMaxLifespanRememberMe();
boolean rememberMeEnabled = realmModel.isRememberMe();
Integer clientIdle = parseIntAttribute(clientModel.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT));
Integer clientMax = parseIntAttribute(clientModel.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN));
if(!rememberMeEnabled) {
// Client idle Timeout validation on Remember me disabled
if (clientIdle != null && clientIdle > realmIdle) {
context.addError(
OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT,
"Client session idle timeout cannot exceed realm SSO session idle timeout.",
Messages.CLIENT_IDLE
);
}
// Max Lifespan validation on Remember me disabled
if (clientMax != null && clientMax > realmMax) {
context.addError(
OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN,
"Client session max lifespan cannot exceed realm SSO session max lifespan.",
Messages.CLIENT_MAXLIFE_SPAN
);
}
} else {
int allowedMaxIdleTimeIfRememberMeEnabled = Math.max(realmIdle, realmRememberIdle);
int allowedMaxSpanIfRememberMeEnabled = Math.max(realmMax,realmRememberMax);
//Client idle Timeout validation on Remember me enabled
if (clientIdle != null && clientIdle > allowedMaxIdleTimeIfRememberMeEnabled) {
context.addError(
OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT,
"Client session idle timeout cannot exceed realm SSO session idle timeout and RememberMe idle timeout.",
Messages.CLIENT_IDLE_REMEMBERME
);
}
// Max Lifespan validation on Remember me enabled
if (clientMax != null && clientMax > allowedMaxSpanIfRememberMeEnabled) {
context.addError(
OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN,
"Client session max lifespan cannot exceed realm SSO session max lifespan and RememberMe Max span.",
Messages.CLIENT_MAXLIFESPAN_REMEMBERME
);
}
}
}
private Integer parseIntAttribute(String value) {
try {
return (value == null || value.isEmpty()) ? null : Integer.parseInt(value);
} catch (NumberFormatException e) {
return null;
}
}
} }

View File

@@ -54,6 +54,7 @@ import org.keycloak.representations.adapters.action.TestAvailabilityAction;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation;
@@ -1051,4 +1052,71 @@ public class ClientTest {
return realm; return realm;
} }
} }
@Test
public void testClientSessionTimeoutValidation() {
ClientRepresentation clientRepresentation = createClient();
clientRepresentation.setAttributes(new HashMap<>());
ClientResource clientResource = managedRealm.admin().clients().get(clientRepresentation.getId());
ClientRepresentation rep = clientResource.toRepresentation();
if (rep.getAttributes() == null) {
rep.setAttributes(new HashMap<>());
}
RealmRepresentation oldRepresentation = managedRealm.admin().toRepresentation();
managedRealm.cleanup().add(rr -> {
rr.update(oldRepresentation);
});
// Remember-Me Disabled
RealmRepresentation realm = managedRealm.admin().toRepresentation();
realm.setRememberMe(false);
realm.setSsoSessionIdleTimeout(300);
realm.setSsoSessionMaxLifespan(600);
managedRealm.admin().update(realm);
// Happy path
rep = clientResource.toRepresentation();
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "200");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "500");
managedRealm.admin().clients().get(rep.getId()).update(rep);
// Failing due to idle time
rep = clientResource.toRepresentation();
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "400");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "500");
createOrUpdateClientExpectingValidationErrors(rep, false,
"Client session idle timeout cannot exceed realm SSO session idle timeout.");
// Fix idle, break max lifespan
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "200");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "700");
createOrUpdateClientExpectingValidationErrors(rep, false,
"Client session max lifespan cannot exceed realm SSO session max lifespan.");
// Remember-Me Enabled
realm = managedRealm.admin().toRepresentation();
realm.setRememberMe(true);
realm.setSsoSessionIdleTimeoutRememberMe(500);
realm.setSsoSessionMaxLifespanRememberMe(900);
managedRealm.admin().update(realm);
// Happy path
rep = clientResource.toRepresentation();
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "400");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "800");
managedRealm.admin().clients().get(rep.getId()).update(rep);
// Failing due to idle time
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "550");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "800");
createOrUpdateClientExpectingValidationErrors(rep, false,
"Client session idle timeout cannot exceed realm SSO session idle timeout and RememberMe idle timeout.");
// Failing due to lifetime
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, "300");
rep.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "950");
createOrUpdateClientExpectingValidationErrors(rep, false,
"Client session max lifespan cannot exceed realm SSO session max lifespan and RememberMe Max span.");
}
} }

View File

@@ -46,6 +46,15 @@ pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs f
duplicatedJwksSettings=The "Use JWKS" switch and the switch "Use JWKS URL" cannot be ON at the same time. duplicatedJwksSettings=The "Use JWKS" switch and the switch "Use JWKS URL" cannot be ON at the same time.
# Shown to an admin when they enter a client specific SSO idle timeout that fails validation
clientIdleExceedsRealmRememberMeIdle=Client session idle timeout cannot exceed realm SSO session idle timeout and RememberMe idle timeout.
# Shown to an admin when they enter a client specific SSO idle timeout that fails validation
clientSessionIdleTimeoutExceedsRealm=Client session idle timeout cannot exceed realm SSO session idle timeout.
# Shown to an admin when they enter a client specific SSO max lifetime that fails validation
clientSessionMaxLifespanExceedsRealm=Client session max lifespan cannot exceed realm SSO session max lifespan.
# Shown to an admin when they enter a client specific SSO max lifetime that fails validation
clientSessionMaxLifespanExceedsRealmRememberMeMaxSpan=Client session max lifespan cannot exceed realm SSO session max lifespan and RememberMe Max span.
error-invalid-value=Invalid value. error-invalid-value=Invalid value.
error-invalid-blank=Please specify value. error-invalid-blank=Please specify value.
error-empty=Please specify value. error-empty=Please specify value.