mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user