Check email and username for duplicated if isLoginWithEmailAllowed
Closes #27297 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
137907f5ef
commit
f970803738
9 changed files with 123 additions and 31 deletions
|
@ -78,6 +78,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
|||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
@ -241,6 +242,12 @@ public class UserResource {
|
|||
} catch (ValidationException pve) {
|
||||
List<ErrorRepresentation> errors = new ArrayList<>();
|
||||
for (ValidationException.Error error : pve.getErrors()) {
|
||||
// some messages are managed directly as before
|
||||
switch (error.getMessage()) {
|
||||
case Messages.MISSING_USERNAME -> throw ErrorResponse.error("User name is missing", Response.Status.BAD_REQUEST);
|
||||
case Messages.USERNAME_EXISTS -> throw ErrorResponse.exists("User exists with same username");
|
||||
case Messages.EMAIL_EXISTS -> throw ErrorResponse.exists("User exists with same email");
|
||||
}
|
||||
errors.add(new ErrorRepresentation(error.getAttribute(), error.getMessage(), error.getMessageParameters()));
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import org.jboss.logging.Logger;
|
|||
import org.jboss.resteasy.reactive.NoCache;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -133,23 +132,6 @@ public class UsersResource {
|
|||
if(realm.isRegistrationEmailAsUsername()) {
|
||||
username = rep.getEmail();
|
||||
}
|
||||
if (ObjectUtil.isBlank(username)) {
|
||||
throw ErrorResponse.error("User name is missing", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Double-check duplicated username and email here due to federation
|
||||
if (session.users().getUserByUsername(realm, username) != null) {
|
||||
throw ErrorResponse.exists("User exists with same username");
|
||||
}
|
||||
if (rep.getEmail() != null && !realm.isDuplicateEmailsAllowed()) {
|
||||
try {
|
||||
if(session.users().getUserByEmail(realm, rep.getEmail()) != null) {
|
||||
throw ErrorResponse.exists("User exists with same email");
|
||||
}
|
||||
} catch (ModelDuplicateException e) {
|
||||
throw ErrorResponse.exists("User exists with same email");
|
||||
}
|
||||
}
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
|
|
|
@ -440,9 +440,14 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
|||
UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
|
||||
|
||||
|
||||
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(UsernameHasValueValidator.ID))
|
||||
metadata.addAttribute(UserModel.USERNAME, -2,
|
||||
new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
||||
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID))
|
||||
.addWriteCondition(DeclarativeUserProfileProviderFactory::editUsernameCondition);
|
||||
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
|
||||
metadata.addAttribute(UserModel.EMAIL, -1,
|
||||
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
|
||||
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID),
|
||||
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
|
||||
.addWriteCondition(DeclarativeUserProfileProviderFactory::editEmailCondition);
|
||||
|
||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||
|
|
|
@ -70,6 +70,13 @@ public class DuplicateEmailValidator implements SimpleValidator {
|
|||
if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) {
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
} else if (realm.isLoginWithEmailAllowed()) {
|
||||
// check for duplicated username
|
||||
userByEmail = session.users().getUserByUsername(realm, value);
|
||||
if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) {
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,12 +64,21 @@ public class DuplicateUsernameValidator implements SimpleValidator {
|
|||
KeycloakSession session = context.getSession();
|
||||
UserModel existing = session.users().getUserByUsername(session.getContext().getRealm(), value);
|
||||
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||
String valueLowercased = value.toLowerCase();
|
||||
|
||||
if (! KeycloakModelUtils.isUsernameCaseSensitive(session.getContext().getRealm())) value = value.toLowerCase();
|
||||
if (! KeycloakModelUtils.isUsernameCaseSensitive(session.getContext().getRealm())) value = valueLowercased;
|
||||
|
||||
if (user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME)) && (existing != null && !existing.getId().equals(user.getId()))) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (existing != null && (user == null || !existing.getId().equals(user.getId()))) {
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
} else if (realm.isLoginWithEmailAllowed() && value.indexOf('@') > 0) {
|
||||
// check the username does not collide with an email
|
||||
existing = session.users().getUserByEmail(realm, valueLowercased);
|
||||
if (existing != null && (user == null || !existing.getId().equals(user.getId()))) {
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
|
|
|
@ -332,6 +332,56 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
|
|||
events.assertEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProfileDuplicateUsernameWithEmail() {
|
||||
getCleanup().addUserId(createUser(TEST_REALM_NAME, "user1@local.com", "password", "user1", "user1", "user1@local.org"));
|
||||
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("john-doh@localhost", "password");
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
updateProfilePage.prepareUpdate().username("user1@local.org").firstName("New first").lastName("New last").email("new@email.com").submit();
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
// assert that form holds submitted values during validation error
|
||||
Assert.assertEquals("New first", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
||||
Assert.assertEquals("new@email.com", updateProfilePage.getEmail());
|
||||
Assert.assertEquals("user1@local.org", updateProfilePage.getUsername());
|
||||
|
||||
Assert.assertEquals("Username already exists.", updateProfilePage.getInputErrors().getUsernameError());
|
||||
|
||||
events.assertEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProfileDuplicatedEmailWithUsername() {
|
||||
getCleanup().addUserId(createUser(TEST_REALM_NAME, "user1@local.com", "password", "user1", "user1", "user1@local.org"));
|
||||
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last")
|
||||
.email("user1@local.com").submit();
|
||||
|
||||
updateProfilePage.assertCurrent();
|
||||
|
||||
// assert that form holds submitted values during validation error
|
||||
Assert.assertEquals("New first", updateProfilePage.getFirstName());
|
||||
Assert.assertEquals("New last", updateProfilePage.getLastName());
|
||||
Assert.assertEquals("user1@local.com", updateProfilePage.getEmail());
|
||||
|
||||
Assert.assertEquals("Email already exists.", updateProfilePage.getInputErrors().getEmailError());
|
||||
|
||||
events.assertEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProfileExpiredCookies() {
|
||||
loginPage.open();
|
||||
|
|
|
@ -325,6 +325,39 @@ public class UserTest extends AbstractAdminTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDuplicatedUsernameWithEmail() {
|
||||
createUser("user1@local.com", "user1@local.org");
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("user1@local.org");
|
||||
user.setEmail("user2@localhost");
|
||||
try (Response response = realm.users().create(user)) {
|
||||
assertEquals(409, response.getStatus());
|
||||
assertAdminEvents.assertEmpty();
|
||||
|
||||
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
|
||||
Assert.assertEquals("User exists with same username", error.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDuplicatedEmailWithUsername() {
|
||||
createUser("user1@local.com", "user1@local.org");
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("user2");
|
||||
user.setEmail("user1@local.com");
|
||||
|
||||
try (Response response = realm.users().create(user)) {
|
||||
assertEquals(409, response.getStatus());
|
||||
assertAdminEvents.assertEmpty();
|
||||
|
||||
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
|
||||
Assert.assertEquals("User exists with same email", error.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
//KEYCLOAK-14611
|
||||
@Test
|
||||
public void createDuplicateEmailWithExistingDuplicates() {
|
||||
|
@ -352,7 +385,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
try (Response response = realm.users().create(user)) {
|
||||
assertEquals(409, response.getStatus());
|
||||
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
|
||||
Assert.assertEquals("User exists with same email", error.getErrorMessage());
|
||||
Assert.assertEquals("User exists with same username or email", error.getErrorMessage());
|
||||
assertAdminEvents.assertEmpty();
|
||||
}
|
||||
}
|
||||
|
@ -2619,7 +2652,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
assertThat(e.getResponse().getStatus(), is(409));
|
||||
|
||||
ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class);
|
||||
Assert.assertEquals("User exists with same username or email", error.getErrorMessage());
|
||||
Assert.assertEquals("User exists with same email", error.getErrorMessage());
|
||||
assertAdminEvents.assertEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,23 +26,20 @@ import org.keycloak.testsuite.Assert;
|
|||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||
import org.keycloak.federation.kerberos.KerberosConfig;
|
||||
import org.keycloak.federation.kerberos.KerberosFederationProviderFactory;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.UserProfileAttributeMetadata;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.testsuite.ActionURIUtils;
|
||||
import org.keycloak.testsuite.KerberosEmbeddedServer;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
|
||||
import org.keycloak.testsuite.forms.VerifyProfileTest;
|
||||
import org.keycloak.testsuite.util.KerberosRule;
|
||||
|
||||
import static org.keycloak.userprofile.UserProfileUtil.USER_METADATA_GROUP;
|
||||
|
@ -181,7 +178,9 @@ public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest {
|
|||
UserRepresentation john = new UserRepresentation();
|
||||
john.setUsername("john");
|
||||
Response response = testRealmResource().users().create(john);
|
||||
Assert.assertEquals(500, response.getStatus());
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
|
||||
Assert.assertEquals("Could not create user", error.getErrorMessage());
|
||||
response.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -202,7 +202,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
private static void testCustomAttributeInAnyContext(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "profiled-user");
|
||||
attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
|
||||
|
@ -243,7 +243,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "profiled-user");
|
||||
attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
|
||||
|
|
Loading…
Reference in a new issue