Add a property to the User Profile Email Validator for max length of the local part

Closes https://github.com/keycloak/keycloak/issues/24273
This commit is contained in:
rmartinc 2023-10-25 11:09:22 +02:00 committed by Marek Posolda
parent 80c71b1951
commit ea398c21da
6 changed files with 96 additions and 12 deletions

View file

@ -202,7 +202,8 @@ image:images/user-profile-validation.png[]
|email |email
|Check if the value has a valid e-mail format. |Check if the value has a valid e-mail format.
| None |
*max-local-length*: an integer to define the maximum length for the local part of the email. It defaults to 64 per specification.
|local-date |local-date
|Check if the value has a valid format based on the realm and/or user locale. |Check if the value has a valid format based on the realm and/or user locale.
@ -293,7 +294,9 @@ The JSON schema is defined as follows:
"edit": [ "admin", "user" ] "edit": [ "admin", "user" ]
}, },
"validations": { "validations": {
"email": {}, "email": {
"max-local-length": 64
},
"length": { "length": {
"max": 255 "max": 255
} }

View file

@ -7,7 +7,15 @@ import org.keycloak.Config;
import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.CASE_INSENSITIVE;
/**
* Email Validator Utility to check email inputs based on
* <a href="https://github.com/hibernate/hibernate-validator/blob/8.0.1.Final/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/AbstractEmailValidator.java">
* hibernate-validator implementation</a>.
*/
public class EmailValidationUtil { public class EmailValidationUtil {
public static final int MAX_LOCAL_PART_LENGTH = 64;
private static final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]"; private static final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]";
private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "(?:[a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")"; private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "(?:[a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")";
/** /**
@ -32,6 +40,10 @@ public class EmailValidationUtil {
public static boolean isValidEmail(String value) { public static boolean isValidEmail(String value) {
return isValidEmail(value, Config.scope("user-profile-declarative-user-profile").getInt(MAX_EMAIL_LOCAL_PART_LENGTH, MAX_LOCAL_PART_LENGTH));
}
public static boolean isValidEmail(String value, int maxEmailLocalPartLength) {
if ( value == null || value.length() == 0 ) { if ( value == null || value.length() == 0 ) {
return false; return false;
} }
@ -49,16 +61,16 @@ public class EmailValidationUtil {
String localPart = stringValue.substring( 0, splitPosition ); String localPart = stringValue.substring( 0, splitPosition );
String domainPart = stringValue.substring( splitPosition + 1 ); String domainPart = stringValue.substring( splitPosition + 1 );
if ( !isValidEmailLocalPart( localPart ) ) { if ( !isValidEmailLocalPart( localPart, maxEmailLocalPartLength ) ) {
return false; return false;
} }
return isValidEmailDomainAddress( domainPart ); return isValidEmailDomainAddress( domainPart );
} }
private static boolean isValidEmailLocalPart(String localPart) { private static boolean isValidEmailLocalPart(String localPart, int maxEmailLocalPartLength) {
if ( localPart.length() > Config.scope("user-profile-declarative-user-profile").getInt(MAX_EMAIL_LOCAL_PART_LENGTH,64) ) { if ( localPart.length() > maxEmailLocalPartLength) {
return false; return false;
} }
Matcher matcher = LOCAL_PART_PATTERN.matcher( localPart ); Matcher matcher = LOCAL_PART_PATTERN.matcher( localPart );

View file

@ -16,15 +16,19 @@
*/ */
package org.keycloak.validate.validators; package org.keycloak.validate.validators;
import java.util.Collections; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.utils.EmailValidationUtil; import org.keycloak.utils.EmailValidationUtil;
import org.keycloak.validate.AbstractStringValidator; import org.keycloak.validate.AbstractStringValidator;
import org.keycloak.validate.ValidationContext; import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError; import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidationResult;
import org.keycloak.validate.ValidatorConfig; import org.keycloak.validate.ValidatorConfig;
/** /**
@ -39,6 +43,7 @@ public class EmailValidator extends AbstractStringValidator implements Configure
public static final String MESSAGE_INVALID_EMAIL = "error-invalid-email"; public static final String MESSAGE_INVALID_EMAIL = "error-invalid-email";
public static final String MAX_LOCAL_PART_LENGTH_PROPERTY = "max-local-length";
@Override @Override
public String getId() { public String getId() {
@ -47,7 +52,14 @@ public class EmailValidator extends AbstractStringValidator implements Configure
@Override @Override
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) { protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
if (!EmailValidationUtil.isValidEmail(value)) { Integer maxEmailLocalPartLength = null;
if (config != null) {
maxEmailLocalPartLength = config.getInt(MAX_LOCAL_PART_LENGTH_PROPERTY);
}
if (!(maxEmailLocalPartLength != null
? EmailValidationUtil.isValidEmail(value, maxEmailLocalPartLength)
: EmailValidationUtil.isValidEmail(value))) {
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value)); context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value));
} }
} }
@ -59,6 +71,25 @@ public class EmailValidator extends AbstractStringValidator implements Configure
@Override @Override
public List<ProviderConfigProperty> getConfigProperties() { public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList(); return ProviderConfigurationBuilder.create().property()
.name(MAX_LOCAL_PART_LENGTH_PROPERTY)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Maximum length for the local part")
.helpText("Maximum length for the local part of the email")
.defaultValue(EmailValidationUtil.MAX_LOCAL_PART_LENGTH)
.required(false)
.add().build();
}
@Override
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
Set<ValidationError> errors = new LinkedHashSet<>();
if (config != null && config.containsKey(MAX_LOCAL_PART_LENGTH_PROPERTY)) {
Integer maxLocalPartLength = config.getInt(MAX_LOCAL_PART_LENGTH_PROPERTY);
if (maxLocalPartLength == null || maxLocalPartLength <= 0) {
errors.add(new ValidationError(ID, MAX_LOCAL_PART_LENGTH_PROPERTY, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(MAX_LOCAL_PART_LENGTH_PROPERTY)));
}
}
return new ValidationResult(errors);
} }
} }

View file

@ -12,6 +12,7 @@ import java.util.regex.Pattern;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.validate.validators.DoubleValidator; import org.keycloak.validate.validators.DoubleValidator;
import org.keycloak.validate.validators.EmailValidator;
import org.keycloak.validate.validators.IntegerValidator; import org.keycloak.validate.validators.IntegerValidator;
import org.keycloak.validate.validators.LengthValidator; import org.keycloak.validate.validators.LengthValidator;
import org.keycloak.validate.validators.OptionsValidator; import org.keycloak.validate.validators.OptionsValidator;
@ -138,6 +139,18 @@ public class BuiltinValidatorsTest {
Assert.assertFalse(validator.validate(" ", "email").isValid()); Assert.assertFalse(validator.validate(" ", "email").isValid());
Assert.assertFalse(validator.validate("adminATexample.org", "email").isValid()); Assert.assertFalse(validator.validate("adminATexample.org", "email").isValid());
Assert.assertTrue(validator.validate("username@keycloak.org", "email", (ValidatorConfig) null).isValid());
Assert.assertTrue(validator.validate("abcd012345678901234567890123456789012345678901234567890123456789@keycloak.org", "email").isValid());
Assert.assertFalse(validator.validate("abcde012345678901234567890123456789012345678901234567890123456789@keycloak.org", "email").isValid());
Assert.assertTrue(validator.validate("abcdef0123456789@keycloak.org", "email",
new ValidatorConfig(ImmutableMap.of(EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, "16"))).isValid());
Assert.assertFalse(validator.validate("abcdefg0123456789@keycloak.org", "email",
new ValidatorConfig(ImmutableMap.of(EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, 16))).isValid());
Assert.assertTrue(validator.validate("ab012345678901234567890123456789@keycloak.org", "email",
new ValidatorConfig(ImmutableMap.of(EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, "32"))).isValid());
Assert.assertFalse(validator.validate("abc012345678901234567890123456789@keycloak.org", "email",
new ValidatorConfig(ImmutableMap.of(EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, 32))).isValid());
} }
@Test @Test

View file

@ -14,6 +14,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.validate.validators.EmailValidator;
import org.keycloak.validate.validators.LengthValidator; import org.keycloak.validate.validators.LengthValidator;
import org.keycloak.validate.validators.NotBlankValidator; import org.keycloak.validate.validators.NotBlankValidator;
import org.keycloak.validate.validators.ValidatorConfigValidator; import org.keycloak.validate.validators.ValidatorConfigValidator;
@ -195,6 +196,24 @@ public class ValidatorTest {
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", "123"))).isValid()); Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", "123"))).isValid());
} }
@Test
public void validateEmailValidator() {
SimpleValidator validator = Validators.emailValidator();
Assert.assertTrue(validator.validateConfig(session, null).isValid());
Assert.assertTrue(validator.validateConfig(session, ValidatorConfig.EMPTY).isValid());
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap(
EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, 128))).isValid());
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap(
EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, "128"))).isValid());
Assert.assertFalse(validator.validateConfig(session, configFromMap(Collections.singletonMap(
EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, null))).isValid());
Assert.assertFalse(validator.validateConfig(session, configFromMap(Collections.singletonMap(
EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, "a"))).isValid());
Assert.assertFalse(validator.validateConfig(session, configFromMap(Collections.singletonMap(
EmailValidator.MAX_LOCAL_PART_LENGTH_PROPERTY, ""))).isValid());
}
@Test @Test
public void validateValidatorConfigMultipleOptions() { public void validateValidatorConfigMultipleOptions() {

View file

@ -362,6 +362,8 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
public void testDefaultProfile() { public void testDefaultProfile() {
setUserProfileConfiguration(null); setUserProfileConfiguration(null);
testingClient.server(TEST_REALM_NAME).run(setEmptyFirstNameAndCustomAttribute());
loginPage.open(); loginPage.open();
loginPage.login("login-test", "password"); loginPage.login("login-test", "password");
@ -719,13 +721,13 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
} }
@Test @Test
public void testEMailRequiredInProfile() { public void testEMailRequiredInProfileWithLocalPartLength() {
setUserProfileConfiguration("{\"attributes\": [" setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"username\"," + PERMISSIONS_ADMIN_ONLY + "}," + "{\"name\": \"username\"," + PERMISSIONS_ADMIN_ONLY + "},"
+ "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\":{\"roles\":[\"user\"]}}" + "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\":{\"roles\":[\"user\"]}, \"validations\": {\"email\": {\"max-local-length\": \"16\"}}}"
+ "]}"); + "]}");
loginPage.open(); loginPage.open();
@ -734,8 +736,12 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
// no email is set => expect verify profile page to be displayed // no email is set => expect verify profile page to be displayed
verifyProfilePage.assertCurrent(); verifyProfilePage.assertCurrent();
// set e-mail with legth 17 => error
verifyProfilePage.updateEmail("abcdefg0123456789@bar.com", "HasNowMailFirst", "HasNowMailLast");
verifyProfilePage.assertCurrent();
// set e-mail, update firstname/lastname and complete login // set e-mail, update firstname/lastname and complete login
verifyProfilePage.updateEmail("foo@bar.com", "HasNowMailFirst", "HasNowMailLast"); verifyProfilePage.updateEmail("abcdef0123456789@bar.com", "HasNowMailFirst", "HasNowMailLast");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
@ -743,7 +749,7 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
UserRepresentation user = getUser(userWithoutEmailId); UserRepresentation user = getUser(userWithoutEmailId);
assertEquals("HasNowMailFirst", user.getFirstName()); assertEquals("HasNowMailFirst", user.getFirstName());
assertEquals("HasNowMailLast", user.getLastName()); assertEquals("HasNowMailLast", user.getLastName());
assertEquals("foo@bar.com", user.getEmail()); assertEquals("abcdef0123456789@bar.com", user.getEmail());
} }
@Test @Test