From 4b194d00bed51458acb3d125eba9a0ba654c930a Mon Sep 17 00:00:00 2001 From: Thore Date: Fri, 12 Apr 2024 00:39:23 +0200 Subject: [PATCH] iso-date validator for the user-profile Adds a new validator in order to be able to validate user-model fields which should be modified/supplied by a datepicker. Closes #11757 Signed-off-by: Thore --- .../topics/users/user-profile.adoc | 4 ++ .../keycloak/validate/BuiltinValidators.java | 5 ++ .../validate/validators/IsoDateValidator.java | 57 +++++++++++++++++++ .../org.keycloak.validate.ValidatorFactory | 1 + .../testsuite/validation/ValidatorTest.java | 40 +++++++++---- 5 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/validate/validators/IsoDateValidator.java diff --git a/docs/documentation/server_admin/topics/users/user-profile.adoc b/docs/documentation/server_admin/topics/users/user-profile.adoc index aee126aca8..9cdd85a410 100644 --- a/docs/documentation/server_admin/topics/users/user-profile.adoc +++ b/docs/documentation/server_admin/topics/users/user-profile.adoc @@ -273,6 +273,10 @@ The list below provides a list of all the built-in validators: |Check if the value has a valid format based on the realm and/or user locale. | None +|iso-date +|Check if the value has a valid format based on ISO 8601. This validator can be used with inputs using the html5-date input type. +| None + |person-name-prohibited-characters | Check if the value is a valid person name as an additional barrier for attacks such as script injection. The validation is based on a default RegEx pattern that blocks characters not common in person names. | diff --git a/server-spi-private/src/main/java/org/keycloak/validate/BuiltinValidators.java b/server-spi-private/src/main/java/org/keycloak/validate/BuiltinValidators.java index 7b80aa1003..d22b9a1bbd 100644 --- a/server-spi-private/src/main/java/org/keycloak/validate/BuiltinValidators.java +++ b/server-spi-private/src/main/java/org/keycloak/validate/BuiltinValidators.java @@ -22,6 +22,7 @@ package org.keycloak.validate; import org.keycloak.validate.validators.DoubleValidator; import org.keycloak.validate.validators.EmailValidator; import org.keycloak.validate.validators.IntegerValidator; +import org.keycloak.validate.validators.IsoDateValidator; import org.keycloak.validate.validators.LengthValidator; import org.keycloak.validate.validators.LocalDateValidator; import org.keycloak.validate.validators.NotBlankValidator; @@ -72,6 +73,10 @@ public class BuiltinValidators { return LocalDateValidator.INSTANCE; } + public static IsoDateValidator isoDateValidator() { + return IsoDateValidator.INSTANCE; + } + public static OptionsValidator optionsValidator() { return OptionsValidator.INSTANCE; } diff --git a/server-spi-private/src/main/java/org/keycloak/validate/validators/IsoDateValidator.java b/server-spi-private/src/main/java/org/keycloak/validate/validators/IsoDateValidator.java new file mode 100644 index 0000000000..09a1b7876a --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/validate/validators/IsoDateValidator.java @@ -0,0 +1,57 @@ +package org.keycloak.validate.validators; + +import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.validate.AbstractStringValidator; +import org.keycloak.validate.ValidationContext; +import org.keycloak.validate.ValidationError; +import org.keycloak.validate.ValidatorConfig; + +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.List; + + +/** + * A date validator that only takes into account the format associated with the current locale. + */ +public class IsoDateValidator extends AbstractStringValidator implements ConfiguredProvider { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + + public static final String MESSAGE_INVALID_DATE = "error-invalid-date"; + + public static final IsoDateValidator INSTANCE = new IsoDateValidator(); + + public static final String ID = "iso-date"; + + @Override + public String getId() { + return ID; + } + + @Override + protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) { + try { + FORMATTER.parse(value); + } catch (DateTimeParseException e) { + context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_DATE, value)); + } + } + + @Override + public String getHelpText() { + return "Validates date in rfc3339/iso8601 format, as provided by the html5-date input."; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + + @Override + protected boolean isIgnoreEmptyValuesConfigured(ValidatorConfig config) { + return true; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory index 48378941f5..1a0dbc73fc 100644 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory @@ -8,3 +8,4 @@ org.keycloak.validate.validators.DoubleValidator org.keycloak.validate.validators.IntegerValidator org.keycloak.validate.validators.LocalDateValidator org.keycloak.validate.validators.OptionsValidator +org.keycloak.validate.validators.IsoDateValidator diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/validation/ValidatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/validation/ValidatorTest.java index 39f58172af..ccfe834bc5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/validation/ValidatorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/validation/ValidatorTest.java @@ -19,12 +19,6 @@ package org.keycloak.testsuite.validation; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.Collections; -import java.util.Locale; - import org.junit.Test; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -32,8 +26,14 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.runonserver.RunOnServer; -import org.keycloak.validate.ValidationContext; import org.keycloak.validate.BuiltinValidators; +import org.keycloak.validate.ValidationContext; + +import java.util.Collections; +import java.util.Locale; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Pedro Igor @@ -45,17 +45,23 @@ public class ValidatorTest extends AbstractTestRealmKeycloakTest { } @Test - public void testDateValidator() { - getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) ValidatorTest::testDateValidator); + public void testLocalDateValidator() { + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) ValidatorTest::testLocalDateValidator); } - private static void testDateValidator(KeycloakSession session) { + @Test + public void testIsoDateValidator() { + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) ValidatorTest::testIsoDateValidator); + } + + private static void testLocalDateValidator(KeycloakSession session) { assertTrue(BuiltinValidators.dateValidator().validate(null, new ValidationContext(session)).isValid()); assertTrue(BuiltinValidators.dateValidator().validate("", new ValidationContext(session)).isValid()); // defaults to Locale.ENGLISH as per default locale selector assertFalse(BuiltinValidators.dateValidator().validate("13/12/2021", new ValidationContext(session)).isValid()); assertFalse(BuiltinValidators.dateValidator().validate("13/12/21", new ValidationContext(session)).isValid()); + assertTrue(BuiltinValidators.dateValidator().validate("12/13/21", new ValidationContext(session)).isValid()); assertTrue(BuiltinValidators.dateValidator().validate("12/13/2021", new ValidationContext(session)).isValid()); RealmModel realm = session.getContext().getRealm(); @@ -76,4 +82,18 @@ public class ValidatorTest extends AbstractTestRealmKeycloakTest { assertFalse(BuiltinValidators.dateValidator().validate("13/12/2021", context).isValid()); } + + private static void testIsoDateValidator(KeycloakSession session) { + assertTrue(BuiltinValidators.isoDateValidator().validate(null, new ValidationContext(session)).isValid()); + assertTrue(BuiltinValidators.isoDateValidator().validate("", new ValidationContext(session)).isValid()); + assertTrue(BuiltinValidators.isoDateValidator().validate("2021-12-13", new ValidationContext(session)).isValid()); + + assertFalse(BuiltinValidators.isoDateValidator().validate("13/12/2021", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("13/12/21", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("12/13/21", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("13.12.21", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("13.12.2021", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("2021-13-12", new ValidationContext(session)).isValid()); + assertFalse(BuiltinValidators.isoDateValidator().validate("21-13-12", new ValidationContext(session)).isValid()); + } }