diff --git a/docs/documentation/server_admin/topics/users/user-profile.adoc b/docs/documentation/server_admin/topics/users/user-profile.adoc index 9cdd85a410..3b7e247deb 100644 --- a/docs/documentation/server_admin/topics/users/user-profile.adoc +++ b/docs/documentation/server_admin/topics/users/user-profile.adoc @@ -285,6 +285,7 @@ The list below provides a list of all the built-in validators: |username-prohibited-characters | Check if the value is a valid username 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 usernames. +When the realm setting `Email as username` is enabled, this validator is skipped to allow email values. | *error-message*: the key of the error message in i18n bundle. If not set a generic message is used. diff --git a/services/src/main/java/org/keycloak/userprofile/validator/UsernameProhibitedCharactersValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/UsernameProhibitedCharactersValidator.java index ae67d196ab..944c20769a 100644 --- a/services/src/main/java/org/keycloak/userprofile/validator/UsernameProhibitedCharactersValidator.java +++ b/services/src/main/java/org/keycloak/userprofile/validator/UsernameProhibitedCharactersValidator.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.validate.AbstractStringValidator; @@ -40,11 +42,11 @@ public class UsernameProhibitedCharactersValidator extends AbstractStringValidat public static final UsernameProhibitedCharactersValidator INSTANCE = new UsernameProhibitedCharactersValidator(); protected static final Pattern PATTERN = Pattern.compile("^[^<>&\"'\\s\\v\\h$%!#?ยง,;:*~/\\\\|^=\\[\\]{}()`\\p{Cntrl}]+$"); - + public static final String MESSAGE_NO_MATCH = "error-username-invalid-character"; - + public static final String CFG_ERROR_MESSAGE = "error-message"; - + private static final List configProperties = new ArrayList<>(); static { @@ -64,12 +66,22 @@ public class UsernameProhibitedCharactersValidator extends AbstractStringValidat @Override protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) { + KeycloakSession session = context.getSession(); + + if (session != null) { + RealmModel realm = session.getContext().getRealm(); + + if (realm.isRegistrationEmailAsUsername()) { + return; + } + } + if (!PATTERN.matcher(value).matches()) { context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, MESSAGE_NO_MATCH))); } } - - + + @Override public String getHelpText() { return "Basic Username validator disallowing bunch of characters we really do not expect in username."; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java index 574155bcef..a035aead53 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java @@ -321,6 +321,7 @@ public class UserProfileTest extends AbstractUserProfileTest { public void testValidation() { getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes); getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation); + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testEmailAsUsernameValidation); } private static void failValidationWhenEmptyAttributes(KeycloakSession session) { @@ -398,7 +399,30 @@ public class UserProfileTest extends AbstractUserProfileTest { assertFalse(profile.getAttributes().validate(UserModel.EMAIL, (Consumer) errors::add)); assertTrue(containsErrorMessage(errors, EmailValidator.MESSAGE_INVALID_EMAIL)); } - + + private static void testEmailAsUsernameValidation(KeycloakSession session) { + Map attributes = new HashMap<>(); + UserProfileProvider provider = session.getProvider(UserProfileProvider.class); + provider.setConfiguration(null); + UserProfile profile; + RealmModel realm = session.getContext().getRealm(); + + try { + realm.setRegistrationEmailAsUsername(true); + attributes.clear(); + attributes.put(UserModel.FIRST_NAME, "Joe"); + attributes.put(UserModel.LAST_NAME, "Doe"); + // valid email but invalid as username + attributes.put(UserModel.EMAIL, "foo%bar@example.com"); + profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes); + profile.validate(); + } catch (ValidationException ve) { + Assert.fail("Should be OK email as username"); + } finally { + realm.setRegistrationEmailAsUsername(false); + } + } + private static boolean containsErrorMessage(List errors, String message){ for(ValidationError err : errors) { if(err.getMessage().equals(message)) { @@ -407,7 +431,7 @@ public class UserProfileTest extends AbstractUserProfileTest { } return false; } - + @Test public void testValidateComplianceWithUserProfile() { @@ -516,14 +540,14 @@ public class UserProfileTest extends AbstractUserProfileTest { assertThat(attributes.nameSet(), containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.LOCALE, "address", "second")); - - + + AttributeGroupMetadata companyAddressGroup = attributes.getMetadata("address").getAttributeGroupMetadata(); assertEquals("companyaddress", companyAddressGroup.getName()); assertEquals("header", companyAddressGroup.getDisplayHeader()); assertEquals("description", companyAddressGroup.getDisplayDescription()); assertNull(companyAddressGroup.getAnnotations()); - + AttributeGroupMetadata groupwithannoGroup = attributes.getMetadata("second").getAttributeGroupMetadata(); assertEquals("groupwithanno", groupwithannoGroup.getName()); assertNull(groupwithannoGroup.getDisplayHeader()); @@ -572,9 +596,9 @@ public class UserProfileTest extends AbstractUserProfileTest { attributesUpdatedOldValues.put(UserModel.FIRST_NAME, "Joe"); attributesUpdatedOldValues.put(UserModel.LAST_NAME, "Doe"); attributesUpdatedOldValues.put(UserModel.EMAIL, "user@keycloak.org"); - + profile.update((attributeName, userModel, oldValue) -> { - assertTrue(attributesUpdated.add(attributeName)); + assertTrue(attributesUpdated.add(attributeName)); assertEquals(attributesUpdatedOldValues.get(attributeName), getSingleValue(oldValue)); assertEquals(attributes.get(attributeName), userModel.getFirstAttribute(attributeName)); }); @@ -593,13 +617,13 @@ public class UserProfileTest extends AbstractUserProfileTest { assertEquals("fixed-business-address", user.getFirstAttribute("business.address")); } - + private static String getSingleValue(List vals) { if(vals==null || vals.isEmpty()) return null; return vals.get(0); } - + @Test public void testReadonlyUpdates() { getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyUpdates);