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:
parent
80c71b1951
commit
ea398c21da
6 changed files with 96 additions and 12 deletions
|
@ -202,7 +202,8 @@ image:images/user-profile-validation.png[]
|
|||
|
||||
|email
|
||||
|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
|
||||
|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" ]
|
||||
},
|
||||
"validations": {
|
||||
"email": {},
|
||||
"email": {
|
||||
"max-local-length": 64
|
||||
},
|
||||
"length": {
|
||||
"max": 255
|
||||
}
|
||||
|
|
|
@ -7,7 +7,15 @@ import org.keycloak.Config;
|
|||
|
||||
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 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_INSIDE_QUOTES_ATOM = "(?:[a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")";
|
||||
/**
|
||||
|
@ -32,6 +40,10 @@ public class EmailValidationUtil {
|
|||
|
||||
|
||||
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 ) {
|
||||
return false;
|
||||
}
|
||||
|
@ -49,16 +61,16 @@ public class EmailValidationUtil {
|
|||
String localPart = stringValue.substring( 0, splitPosition );
|
||||
String domainPart = stringValue.substring( splitPosition + 1 );
|
||||
|
||||
if ( !isValidEmailLocalPart( localPart ) ) {
|
||||
if ( !isValidEmailLocalPart( localPart, maxEmailLocalPartLength ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Matcher matcher = LOCAL_PART_PATTERN.matcher( localPart );
|
||||
|
|
|
@ -16,15 +16,19 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.utils.EmailValidationUtil;
|
||||
import org.keycloak.validate.AbstractStringValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
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 MAX_LOCAL_PART_LENGTH_PROPERTY = "max-local-length";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
|
@ -47,7 +52,14 @@ public class EmailValidator extends AbstractStringValidator implements Configure
|
|||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +71,25 @@ public class EmailValidator extends AbstractStringValidator implements Configure
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.regex.Pattern;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.validate.validators.DoubleValidator;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
import org.keycloak.validate.validators.IntegerValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
import org.keycloak.validate.validators.OptionsValidator;
|
||||
|
@ -138,6 +139,18 @@ public class BuiltinValidatorsTest {
|
|||
|
||||
Assert.assertFalse(validator.validate(" ", "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
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
import org.keycloak.validate.validators.NotBlankValidator;
|
||||
import org.keycloak.validate.validators.ValidatorConfigValidator;
|
||||
|
@ -195,6 +196,24 @@ public class ValidatorTest {
|
|||
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
|
||||
public void validateValidatorConfigMultipleOptions() {
|
||||
|
||||
|
|
|
@ -362,6 +362,8 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
public void testDefaultProfile() {
|
||||
setUserProfileConfiguration(null);
|
||||
|
||||
testingClient.server(TEST_REALM_NAME).run(setEmptyFirstNameAndCustomAttribute());
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test", "password");
|
||||
|
||||
|
@ -719,13 +721,13 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testEMailRequiredInProfile() {
|
||||
public void testEMailRequiredInProfileWithLocalPartLength() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"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();
|
||||
|
@ -734,8 +736,12 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
// no email is set => expect verify profile page to be displayed
|
||||
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
|
||||
verifyProfilePage.updateEmail("foo@bar.com", "HasNowMailFirst", "HasNowMailLast");
|
||||
verifyProfilePage.updateEmail("abcdef0123456789@bar.com", "HasNowMailFirst", "HasNowMailLast");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
@ -743,7 +749,7 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
|||
UserRepresentation user = getUser(userWithoutEmailId);
|
||||
assertEquals("HasNowMailFirst", user.getFirstName());
|
||||
assertEquals("HasNowMailLast", user.getLastName());
|
||||
assertEquals("foo@bar.com", user.getEmail());
|
||||
assertEquals("abcdef0123456789@bar.com", user.getEmail());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue