Merge pull request #3630 from sldab/duplicate-email-support

KEYCLOAK-4059 Support for duplicate emails
This commit is contained in:
Marek Posolda 2016-12-19 15:37:18 +01:00 committed by GitHub
commit c6363aa146
53 changed files with 947 additions and 79 deletions

View file

@ -54,6 +54,8 @@ public class RealmRepresentation {
protected Boolean registrationEmailAsUsername; protected Boolean registrationEmailAsUsername;
protected Boolean rememberMe; protected Boolean rememberMe;
protected Boolean verifyEmail; protected Boolean verifyEmail;
protected Boolean loginWithEmailAllowed;
protected Boolean duplicateEmailsAllowed;
protected Boolean resetPasswordAllowed; protected Boolean resetPasswordAllowed;
protected Boolean editUsernameAllowed; protected Boolean editUsernameAllowed;
@ -418,6 +420,22 @@ public class RealmRepresentation {
public void setVerifyEmail(Boolean verifyEmail) { public void setVerifyEmail(Boolean verifyEmail) {
this.verifyEmail = verifyEmail; this.verifyEmail = verifyEmail;
} }
public Boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed;
}
public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) {
this.loginWithEmailAllowed = loginWithEmailAllowed;
}
public Boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed;
}
public void setDuplicateEmailsAllowed(Boolean duplicateEmailsAllowed) {
this.duplicateEmailsAllowed = duplicateEmailsAllowed;
}
public Boolean isResetPasswordAllowed() { public Boolean isResetPasswordAllowed() {
return resetPasswordAllowed; return resetPasswordAllowed;

View file

@ -164,7 +164,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
// throw ModelDuplicateException if there is different user in model with same email // throw ModelDuplicateException if there is different user in model with same email
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) { protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
if (email == null) return; if (email == null || realm.isDuplicateEmailsAllowed()) return;
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) { if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
// lowercase before search // lowercase before search
email = KeycloakModelUtils.toLowerCaseSafe(email); email = KeycloakModelUtils.toLowerCaseSafe(email);

View file

@ -305,6 +305,30 @@ public class RealmAdapter implements CachedRealmModel {
updated.setVerifyEmail(verifyEmail); updated.setVerifyEmail(verifyEmail);
} }
@Override
public boolean isLoginWithEmailAllowed() {
if (isUpdated()) return updated.isLoginWithEmailAllowed();
return cached.isLoginWithEmailAllowed();
}
@Override
public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
getDelegateForUpdate();
updated.setLoginWithEmailAllowed(loginWithEmailAllowed);
}
@Override
public boolean isDuplicateEmailsAllowed() {
if (isUpdated()) return updated.isDuplicateEmailsAllowed();
return cached.isDuplicateEmailsAllowed();
}
@Override
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
getDelegateForUpdate();
updated.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
}
@Override @Override
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {
if (isUpdated()) return updated.isResetPasswordAllowed(); if (isUpdated()) return updated.isResetPasswordAllowed();

View file

@ -34,9 +34,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -61,6 +58,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected boolean registrationEmailAsUsername; protected boolean registrationEmailAsUsername;
protected boolean rememberMe; protected boolean rememberMe;
protected boolean verifyEmail; protected boolean verifyEmail;
protected boolean loginWithEmailAllowed;
protected boolean duplicateEmailsAllowed;
protected boolean resetPasswordAllowed; protected boolean resetPasswordAllowed;
protected boolean identityFederationEnabled; protected boolean identityFederationEnabled;
protected boolean editUsernameAllowed; protected boolean editUsernameAllowed;
@ -150,6 +149,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
registrationEmailAsUsername = model.isRegistrationEmailAsUsername(); registrationEmailAsUsername = model.isRegistrationEmailAsUsername();
rememberMe = model.isRememberMe(); rememberMe = model.isRememberMe();
verifyEmail = model.isVerifyEmail(); verifyEmail = model.isVerifyEmail();
loginWithEmailAllowed = model.isLoginWithEmailAllowed();
duplicateEmailsAllowed = model.isDuplicateEmailsAllowed();
resetPasswordAllowed = model.isResetPasswordAllowed(); resetPasswordAllowed = model.isResetPasswordAllowed();
identityFederationEnabled = model.isIdentityFederationEnabled(); identityFederationEnabled = model.isIdentityFederationEnabled();
editUsernameAllowed = model.isEditUsernameAllowed(); editUsernameAllowed = model.isEditUsernameAllowed();
@ -340,6 +341,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
public boolean isVerifyEmail() { public boolean isVerifyEmail() {
return verifyEmail; return verifyEmail;
} }
public boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed;
}
public boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed;
}
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {
return resetPasswordAllowed; return resetPasswordAllowed;

View file

@ -480,7 +480,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
query.setParameter("email", email.toLowerCase()); query.setParameter("email", email.toLowerCase());
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", realm.getId());
List<UserEntity> results = query.getResultList(); List<UserEntity> results = query.getResultList();
return results.isEmpty() ? null : new UserAdapter(session, realm, em, results.get(0));
if (results.isEmpty()) return null;
ensureEmailConstraint(results, realm);
return new UserAdapter(session, realm, em, results.get(0));
} }
@Override @Override
@ -880,7 +885,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
return toModel(results.get(0)); return toModel(results.get(0));
} }
// Could override this to provide a custom behavior.
protected void ensureEmailConstraint(List<UserEntity> users, RealmModel realm) {
UserEntity user = users.get(0);
if (users.size() > 1) {
// Realm settings have been changed from allowing duplicate emails to not allowing them
// but duplicates haven't been removed.
throw new ModelDuplicateException("Multiple users with email '" + user.getEmail() + "' exist in Keycloak.");
}
if (realm.isDuplicateEmailsAllowed()) {
return;
}
if (user.getEmail() != null && !user.getEmail().equals(user.getEmailConstraint())) {
// Realm settings have been changed from allowing duplicate emails to not allowing them.
// We need to update the email constraint to reflect this change in the user entities.
user.setEmailConstraint(user.getEmail());
em.persist(user);
}
}
} }

View file

@ -169,6 +169,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override @Override
public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) {
realm.setRegistrationEmailAsUsername(registrationEmailAsUsername); realm.setRegistrationEmailAsUsername(registrationEmailAsUsername);
if (registrationEmailAsUsername) realm.setDuplicateEmailsAllowed(false);
em.flush(); em.flush();
} }
@ -347,6 +348,33 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
realm.setVerifyEmail(verifyEmail); realm.setVerifyEmail(verifyEmail);
em.flush(); em.flush();
} }
@Override
public boolean isLoginWithEmailAllowed() {
return realm.isLoginWithEmailAllowed();
}
@Override
public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
realm.setLoginWithEmailAllowed(loginWithEmailAllowed);
if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false);
em.flush();
}
@Override
public boolean isDuplicateEmailsAllowed() {
return realm.isDuplicateEmailsAllowed();
}
@Override
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
realm.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
if (duplicateEmailsAllowed) {
realm.setLoginWithEmailAllowed(false);
realm.setRegistrationEmailAsUsername(false);
}
em.flush();
}
@Override @Override
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {

View file

@ -276,7 +276,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override @Override
public void setEmail(String email) { public void setEmail(String email) {
email = KeycloakModelUtils.toLowerCaseSafe(email); email = KeycloakModelUtils.toLowerCaseSafe(email);
user.setEmail(email); user.setEmail(email, realm.isDuplicateEmailsAllowed());
} }
@Override @Override

View file

@ -73,6 +73,10 @@ public class RealmEntity {
protected boolean verifyEmail; protected boolean verifyEmail;
@Column(name="RESET_PASSWORD_ALLOWED") @Column(name="RESET_PASSWORD_ALLOWED")
protected boolean resetPasswordAllowed; protected boolean resetPasswordAllowed;
@Column(name="LOGIN_WITH_EMAIL_ALLOWED")
protected boolean loginWithEmailAllowed;
@Column(name="DUPLICATE_EMAILS_ALLOWED")
protected boolean duplicateEmailsAllowed;
@Column(name="REMEMBER_ME") @Column(name="REMEMBER_ME")
protected boolean rememberMe; protected boolean rememberMe;
@ -287,6 +291,22 @@ public class RealmEntity {
public void setVerifyEmail(boolean verifyEmail) { public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail; this.verifyEmail = verifyEmail;
} }
public boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed;
}
public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
this.loginWithEmailAllowed = loginWithEmailAllowed;
}
public boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed;
}
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
this.duplicateEmailsAllowed = duplicateEmailsAllowed;
}
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {
return resetPasswordAllowed; return resetPasswordAllowed;

View file

@ -78,7 +78,7 @@ public class UserEntity {
@Column(name = "EMAIL_VERIFIED") @Column(name = "EMAIL_VERIFIED")
protected boolean emailVerified; protected boolean emailVerified;
// Hack just to workaround the fact that on MS-SQL you can't have unique constraint with multiple NULL values TODO: Find better solution (like unique index with 'where' but that's proprietary) // This is necessary to be able to dynamically switch unique email constraints on and off in the realm settings
@Column(name = "EMAIL_CONSTRAINT") @Column(name = "EMAIL_CONSTRAINT")
protected String emailConstraint = KeycloakModelUtils.generateId(); protected String emailConstraint = KeycloakModelUtils.generateId();
@ -144,9 +144,9 @@ public class UserEntity {
return email; return email;
} }
public void setEmail(String email) { public void setEmail(String email, boolean allowDuplicate) {
this.email = email; this.email = email;
this.emailConstraint = email != null ? email : KeycloakModelUtils.generateId(); this.emailConstraint = email == null || allowDuplicate ? KeycloakModelUtils.generateId() : email;
} }
public boolean isEnabled() { public boolean isEnabled() {

View file

@ -98,5 +98,16 @@
<addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/> <addUniqueConstraint columnNames="NAME,CLIENT_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2-2" tableName="KEYCLOAK_ROLE"/>
<modifyDataType tableName="KEYCLOAK_ROLE" columnName="DESCRIPTION" newDataType="NVARCHAR(255)"/> <modifyDataType tableName="KEYCLOAK_ROLE" columnName="DESCRIPTION" newDataType="NVARCHAR(255)"/>
</changeSet> </changeSet>
<changeSet author="slawomir@dabek.name" id="2.5.0-duplicate-email-support">
<addColumn tableName="REALM">
<column name="LOGIN_WITH_EMAIL_ALLOWED" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="DUPLICATE_EMAILS_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -17,8 +17,10 @@
package org.keycloak.connections.mongo.updater.impl.updates; package org.keycloak.connections.mongo.updater.impl.updates;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
@ -40,6 +42,16 @@ public class Update2_5_0 extends AbstractMigrateUserFedToComponent {
for (ProviderFactory factory : factories) { for (ProviderFactory factory : factories) {
portUserFedToComponent(factory.getId()); portUserFedToComponent(factory.getId());
} }
DBCollection realms = db.getCollection("realms");
try (DBCursor realmsCursor = realms.find()) {
while (realmsCursor.hasNext()) {
BasicDBObject realm = (BasicDBObject) realmsCursor.next();
realm.append("loginWithEmailAllowed", true);
realm.append("duplicateEmailsAllowed", false);
realms.save(realm);
}
}
} }
} }

View file

@ -60,6 +60,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.keycloak.models.mongo.keycloak.entities.UserEntity;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -111,13 +112,13 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
.and("email").is(email.toLowerCase()) .and("email").is(email.toLowerCase())
.and("realmId").is(realm.getId()) .and("realmId").is(realm.getId())
.get(); .get();
MongoUserEntity user = getMongoStore().loadSingleEntity(MongoUserEntity.class, query, invocationContext); List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
if (user == null) { if (users.isEmpty()) return null;
return null;
} else { ensureEmailConstraint(users, realm);
return new UserAdapter(session, realm, user, invocationContext);
} return new UserAdapter(session, realm, users.get(0), invocationContext);
} }
@Override @Override
@ -817,4 +818,26 @@ public class MongoUserProvider implements UserProvider, UserCredentialStore {
if (update) getMongoStore().updateEntity(mongoUser, invocationContext); if (update) getMongoStore().updateEntity(mongoUser, invocationContext);
return credModel; return credModel;
} }
// Could override this to provide a custom behavior.
protected void ensureEmailConstraint(List<MongoUserEntity> users, RealmModel realm) {
MongoUserEntity user = users.get(0);
if (users.size() > 1) {
// Realm settings have been changed from allowing duplicate emails to not allowing them
// but duplicates haven't been removed.
throw new ModelDuplicateException("Multiple users with email '" + user.getEmail() + "' exist in Keycloak.");
}
if (realm.isDuplicateEmailsAllowed()) {
return;
}
if (user.getEmail() != null && user.getEmailIndex() == null) {
// Realm settings have been changed from allowing duplicate emails to not allowing them.
// We need to update the email index to reflect this change in the user entities.
user.setEmail(user.getEmail(), false);
getMongoStore().updateEntity(user, invocationContext);
}
}
} }

View file

@ -157,12 +157,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateRealm(); updateRealm();
} }
@Override
public boolean isRegistrationEmailAsUsername() { public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername(); return realm.isRegistrationEmailAsUsername();
} }
@Override
public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) { public void setRegistrationEmailAsUsername(boolean registrationEmailAsUsername) {
realm.setRegistrationEmailAsUsername(registrationEmailAsUsername); realm.setRegistrationEmailAsUsername(registrationEmailAsUsername);
if (registrationEmailAsUsername) realm.setDuplicateEmailsAllowed(false);
updateRealm(); updateRealm();
} }
@ -266,6 +269,33 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
realm.setVerifyEmail(verifyEmail); realm.setVerifyEmail(verifyEmail);
updateRealm(); updateRealm();
} }
@Override
public boolean isLoginWithEmailAllowed() {
return realm.isLoginWithEmailAllowed();
}
@Override
public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
realm.setLoginWithEmailAllowed(loginWithEmailAllowed);
if (loginWithEmailAllowed) realm.setDuplicateEmailsAllowed(false);
updateRealm();
}
@Override
public boolean isDuplicateEmailsAllowed() {
return realm.isDuplicateEmailsAllowed();
}
@Override
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
realm.setDuplicateEmailsAllowed(duplicateEmailsAllowed);
if (duplicateEmailsAllowed) {
realm.setLoginWithEmailAllowed(false);
realm.setRegistrationEmailAsUsername(false);
}
updateRealm();
}
@Override @Override
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {

View file

@ -124,8 +124,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
@Override @Override
public void setEmail(String email) { public void setEmail(String email) {
email = KeycloakModelUtils.toLowerCaseSafe(email); email = KeycloakModelUtils.toLowerCaseSafe(email);
user.setEmail(email, realm.isDuplicateEmailsAllowed());
user.setEmail(email);
updateUser(); updateUser();
} }

View file

@ -29,13 +29,6 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
@MongoCollection(collectionName = "users") @MongoCollection(collectionName = "users")
public class MongoUserEntity extends UserEntity implements MongoIdentifiableEntity { public class MongoUserEntity extends UserEntity implements MongoIdentifiableEntity {
public String getEmailIndex() {
return getEmail() != null ? getRealmId() + "//" + getEmail() : null;
}
public void setEmailIndex(String ignored) {
}
@Override @Override
public void afterRemove(MongoStoreInvocationContext context) { public void afterRemove(MongoStoreInvocationContext context) {
// Remove all consents of this user // Remove all consents of this user

View file

@ -37,6 +37,8 @@ public class RealmEntity extends AbstractIdentifiableEntity {
protected boolean registrationEmailAsUsername; protected boolean registrationEmailAsUsername;
private boolean rememberMe; private boolean rememberMe;
private boolean verifyEmail; private boolean verifyEmail;
private boolean loginWithEmailAllowed;
private boolean duplicateEmailsAllowed;
private boolean resetPasswordAllowed; private boolean resetPasswordAllowed;
private String passwordPolicy; private String passwordPolicy;
@ -186,6 +188,22 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setVerifyEmail(boolean verifyEmail) { public void setVerifyEmail(boolean verifyEmail) {
this.verifyEmail = verifyEmail; this.verifyEmail = verifyEmail;
} }
public boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed;
}
public void setLoginWithEmailAllowed(boolean loginWithEmailAllowed) {
this.loginWithEmailAllowed = loginWithEmailAllowed;
}
public boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed;
}
public void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed) {
this.duplicateEmailsAllowed = duplicateEmailsAllowed;
}
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {
return resetPasswordAllowed; return resetPasswordAllowed;

View file

@ -31,6 +31,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private String firstName; private String firstName;
private String lastName; private String lastName;
private String email; private String email;
private String emailIndex;
private boolean emailVerified; private boolean emailVerified;
private boolean enabled; private boolean enabled;
@ -82,11 +83,25 @@ public class UserEntity extends AbstractIdentifiableEntity {
public String getEmail() { public String getEmail() {
return email; return email;
} }
@Deprecated // called upon deserialization only
public void setEmail(String email) { public void setEmail(String email) {
this.email = email; this.email = email;
} }
public void setEmail(String email, boolean allowDuplicate) {
this.email = email;
this.emailIndex = email == null || allowDuplicate ? null : getRealmId() + "//" + email;
}
public void setEmailIndex(String index) {
this.emailIndex = index;
}
public String getEmailIndex() {
return emailIndex;
}
public boolean isEmailVerified() { public boolean isEmailVerified() {
return emailVerified; return emailVerified;
} }

View file

@ -188,14 +188,14 @@ public final class KeycloakModelUtils {
} }
/** /**
* Try to find user by username or email * Try to find user by username or email for authentication
* *
* @param realm realm * @param realm realm
* @param username username or email of user * @param username username or email of user
* @return found user * @return found user
*/ */
public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) { public static UserModel findUserByNameOrEmail(KeycloakSession session, RealmModel realm, String username) {
if (username.indexOf('@') != -1) { if (realm.isLoginWithEmailAllowed() && username.indexOf('@') != -1) {
UserModel user = session.users().getUserByEmail(username, realm); UserModel user = session.users().getUserByEmail(username, realm);
if (user != null) { if (user != null) {
return user; return user;

View file

@ -292,6 +292,8 @@ public class ModelToRepresentation {
rep.setAdminEventsDetailsEnabled(realm.isAdminEventsDetailsEnabled()); rep.setAdminEventsDetailsEnabled(realm.isAdminEventsDetailsEnabled());
rep.setVerifyEmail(realm.isVerifyEmail()); rep.setVerifyEmail(realm.isVerifyEmail());
rep.setLoginWithEmailAllowed(realm.isLoginWithEmailAllowed());
rep.setDuplicateEmailsAllowed(realm.isDuplicateEmailsAllowed());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed()); rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed()); rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken()); rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());

View file

@ -184,6 +184,8 @@ public class RepresentationToModel {
newRealm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); newRealm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername());
if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe()); if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail());
if (rep.isLoginWithEmailAllowed() != null) newRealm.setLoginWithEmailAllowed(rep.isLoginWithEmailAllowed());
if (rep.isDuplicateEmailsAllowed() != null) newRealm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed()); if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme()); if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
@ -785,6 +787,8 @@ public class RepresentationToModel {
if (rep.isRegistrationEmailAsUsername() != null) realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername()); if (rep.isRegistrationEmailAsUsername() != null) realm.setRegistrationEmailAsUsername(rep.isRegistrationEmailAsUsername());
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe()); if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
if (rep.isLoginWithEmailAllowed() != null) realm.setLoginWithEmailAllowed(rep.isLoginWithEmailAllowed());
if (rep.isDuplicateEmailsAllowed() != null) realm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed()); if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));

View file

@ -23,10 +23,6 @@ import org.keycloak.provider.ProviderEvent;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -149,6 +145,14 @@ public interface RealmModel extends RoleContainerModel {
boolean isVerifyEmail(); boolean isVerifyEmail();
void setVerifyEmail(boolean verifyEmail); void setVerifyEmail(boolean verifyEmail);
boolean isLoginWithEmailAllowed();
void setLoginWithEmailAllowed(boolean loginWithEmailAllowed);
boolean isDuplicateEmailsAllowed();
void setDuplicateEmailsAllowed(boolean duplicateEmailsAllowed);
boolean isResetPasswordAllowed(); boolean isResetPasswordAllowed();

View file

@ -119,7 +119,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
// Could be overriden to detect duplication based on other criterias (firstName, lastName, ...) // Could be overriden to detect duplication based on other criterias (firstName, lastName, ...)
protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, String username, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { protected ExistingUserInfo checkExistingUser(AuthenticationFlowContext context, String username, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
if (brokerContext.getEmail() != null) { if (brokerContext.getEmail() != null && !context.getRealm().isDuplicateEmailsAllowed()) {
UserModel existingUser = context.getSession().users().getUserByEmail(brokerContext.getEmail(), context.getRealm()); UserModel existingUser = context.getSession().users().getUserByEmail(brokerContext.getEmail(), context.getRealm());
if (existingUser != null) { if (existingUser != null) {
return new ExistingUserInfo(existingUser.getId(), UserModel.EMAIL, existingUser.getEmail()); return new ExistingUserInfo(existingUser.getId(), UserModel.EMAIL, existingUser.getEmail());

View file

@ -80,10 +80,11 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
return; return;
} }
UserModel user = context.getSession().users().getUserByUsername(username, context.getRealm()); RealmModel realm = context.getRealm();
if (user == null && username.contains("@")) { UserModel user = context.getSession().users().getUserByUsername(username, realm);
user = context.getSession().users().getUserByEmail(username, context.getRealm()); if (user == null && realm.isLoginWithEmailAllowed() && username.contains("@")) {
user = context.getSession().users().getUserByEmail(username, realm);
} }
context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);

View file

@ -83,7 +83,7 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
emailValid = false; emailValid = false;
} }
if (emailValid && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) { if (emailValid && !context.getRealm().isDuplicateEmailsAllowed() && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
eventError = Errors.EMAIL_IN_USE; eventError = Errors.EMAIL_IN_USE;
formData.remove(Validation.FIELD_EMAIL); formData.remove(Validation.FIELD_EMAIL);
context.getEvent().detail(Details.EMAIL, email); context.getEvent().detail(Details.EMAIL, email);

View file

@ -86,7 +86,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
context.validationError(formData, errors); context.validationError(formData, errors);
return; return;
} }
if (email != null && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) { if (email != null && !context.getRealm().isDuplicateEmailsAllowed() && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
context.error(Errors.EMAIL_IN_USE); context.error(Errors.EMAIL_IN_USE);
formData.remove(Validation.FIELD_EMAIL); formData.remove(Validation.FIELD_EMAIL);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS)); errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));

View file

@ -104,16 +104,18 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) { if (emailChanged) {
UserModel userByEmail = session.users().getUserByEmail(email, realm); if (!realm.isDuplicateEmailsAllowed()) {
UserModel userByEmail = session.users().getUserByEmail(email, realm);
// check for duplicated email // check for duplicated email
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
Response challenge = context.form() Response challenge = context.form()
.setError(Messages.EMAIL_EXISTS) .setError(Messages.EMAIL_EXISTS)
.setFormData(formData) .setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE); .createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge); context.challenge(challenge);
return; return;
}
} }
user.setEmail(email); user.setEmail(email);

View file

@ -64,6 +64,10 @@ public class RealmBean {
public boolean isRegistrationEmailAsUsername() { public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername(); return realm.isRegistrationEmailAsUsername();
} }
public boolean isLoginWithEmailAllowed() {
return realm.isLoginWithEmailAllowed();
}
public boolean isResetPasswordAllowed() { public boolean isResetPasswordAllowed() {
return realm.isResetPasswordAllowed(); return realm.isResetPasswordAllowed();

View file

@ -42,7 +42,7 @@ public abstract class AbstractPartialImport<T> implements PartialImport<T> {
public abstract String getName(T resourceRep); public abstract String getName(T resourceRep);
public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep); public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep); public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract String existsMessage(T resourceRep); public abstract String existsMessage(RealmModel realm, T resourceRep);
public abstract ResourceType getResourceType(); public abstract ResourceType getResourceType();
public abstract void remove(RealmModel realm, KeycloakSession session, T resourceRep); public abstract void remove(RealmModel realm, KeycloakSession session, T resourceRep);
public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep); public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep);
@ -59,7 +59,7 @@ public abstract class AbstractPartialImport<T> implements PartialImport<T> {
switch (partialImportRep.getPolicy()) { switch (partialImportRep.getPolicy()) {
case SKIP: toSkip.add(resourceRep); break; case SKIP: toSkip.add(resourceRep); break;
case OVERWRITE: toOverwrite.add(resourceRep); break; case OVERWRITE: toOverwrite.add(resourceRep); break;
default: throw existsError(existsMessage(resourceRep)); default: throw existsError(existsMessage(realm, resourceRep));
} }
} }
} }

View file

@ -56,7 +56,7 @@ public class ClientsPartialImport extends AbstractPartialImport<ClientRepresenta
} }
@Override @Override
public String existsMessage(ClientRepresentation clientRep) { public String existsMessage(RealmModel realm, ClientRepresentation clientRep) {
return "Client id '" + getName(clientRep) + "' already exists"; return "Client id '" + getName(clientRep) + "' already exists";
} }

View file

@ -59,7 +59,7 @@ public class GroupsPartialImport extends AbstractPartialImport<GroupRepresentati
} }
@Override @Override
public String existsMessage(GroupRepresentation groupRep) { public String existsMessage(RealmModel realm, GroupRepresentation groupRep) {
return "Group '" + groupRep.getPath() + "' already exists"; return "Group '" + groupRep.getPath() + "' already exists";
} }

View file

@ -55,7 +55,7 @@ public class IdentityProvidersPartialImport extends AbstractPartialImport<Identi
} }
@Override @Override
public String existsMessage(IdentityProviderRepresentation idpRep) { public String existsMessage(RealmModel realm, IdentityProviderRepresentation idpRep) {
return "Identity Provider '" + getName(idpRep) + "' already exists."; return "Identity Provider '" + getName(idpRep) + "' already exists.";
} }

View file

@ -19,8 +19,10 @@ package org.keycloak.partialimport;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.AdminEventBuilder;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -86,7 +88,11 @@ public class PartialImportManager {
} }
if (session.getTransactionManager().isActive()) { if (session.getTransactionManager().isActive()) {
session.getTransactionManager().commit(); try {
session.getTransactionManager().commit();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists(e.getLocalizedMessage());
}
} }
return Response.ok(results).build(); return Response.ok(results).build();

View file

@ -73,7 +73,7 @@ public class RealmRolesPartialImport extends AbstractPartialImport<RoleRepresent
} }
@Override @Override
public String existsMessage(RoleRepresentation roleRep) { public String existsMessage(RealmModel realm, RoleRepresentation roleRep) {
return "Realm role '" + getName(roleRep) + "' already exists."; return "Realm role '" + getName(roleRep) + "' already exists.";
} }

View file

@ -60,10 +60,12 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
String userName = user.getUsername(); String userName = user.getUsername();
if (userName != null) { if (userName != null) {
return session.users().getUserByUsername(userName, realm).getId(); return session.users().getUserByUsername(userName, realm).getId();
} else { } else if (!realm.isDuplicateEmailsAllowed()) {
String email = user.getEmail(); String email = user.getEmail();
return session.users().getUserByEmail(email, realm).getId(); return session.users().getUserByEmail(email, realm).getId();
} }
return null;
} }
@Override @Override
@ -76,13 +78,13 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
} }
private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) { private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
return (user.getEmail() != null) && return (user.getEmail() != null) && !realm.isDuplicateEmailsAllowed() &&
(session.users().getUserByEmail(user.getEmail(), realm) != null); (session.users().getUserByEmail(user.getEmail(), realm) != null);
} }
@Override @Override
public String existsMessage(UserRepresentation user) { public String existsMessage(RealmModel realm, UserRepresentation user) {
if (user.getEmail() == null) { if (user.getEmail() == null || !realm.isDuplicateEmailsAllowed()) {
return "User with user name " + getName(user) + " already exists."; return "User with user name " + getName(user) + " already exists.";
} }
@ -97,12 +99,13 @@ public class UsersPartialImport extends AbstractPartialImport<UserRepresentation
@Override @Override
public void remove(RealmModel realm, KeycloakSession session, UserRepresentation user) { public void remove(RealmModel realm, KeycloakSession session, UserRepresentation user) {
UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm); UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm);
if (userModel == null) { if (userModel == null && !realm.isDuplicateEmailsAllowed()) {
userModel = session.users().getUserByEmail(user.getEmail(), realm); userModel = session.users().getUserByEmail(user.getEmail(), realm);
} }
if (userModel != null) {
boolean success = new UserManager(session).removeUser(realm, userModel); boolean success = new UserManager(session).removeUser(realm, userModel);
if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user)); if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user));
}
} }
@Override @Override

View file

@ -220,6 +220,7 @@ public class RealmManager implements RealmImporter {
realm.setFailureFactor(30); realm.setFailureFactor(30);
realm.setSslRequired(SslRequired.EXTERNAL); realm.setSslRequired(SslRequired.EXTERNAL);
realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
realm.setLoginWithEmailAllowed(true);
realm.setEventsListeners(Collections.singleton("jboss-logging")); realm.setEventsListeners(Collections.singleton("jboss-logging"));

View file

@ -400,7 +400,7 @@ public class AccountService extends AbstractSecuredLocalService {
String email = formData.getFirst("email"); String email = formData.getFirst("email");
String oldEmail = user.getEmail(); String oldEmail = user.getEmail();
boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null; boolean emailChanged = oldEmail != null ? !oldEmail.equals(email) : email != null;
if (emailChanged) { if (emailChanged && !realm.isDuplicateEmailsAllowed()) {
UserModel existing = session.users().getUserByEmail(email, realm); UserModel existing = session.users().getUserByEmail(email, realm);
if (existing != null && !existing.getId().equals(user.getId())) { if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.EMAIL_EXISTS); throw new ModelDuplicateException(Messages.EMAIL_EXISTS);
@ -419,9 +419,11 @@ public class AccountService extends AbstractSecuredLocalService {
} }
if (realm.isRegistrationEmailAsUsername()) { if (realm.isRegistrationEmailAsUsername()) {
UserModel existing = session.users().getUserByEmail(email, realm); if (!realm.isDuplicateEmailsAllowed()) {
if (existing != null && !existing.getId().equals(user.getId())) { UserModel existing = session.users().getUserByEmail(email, realm);
throw new ModelDuplicateException(Messages.USERNAME_EXISTS); if (existing != null && !existing.getId().equals(user.getId())) {
throw new ModelDuplicateException(Messages.USERNAME_EXISTS);
}
} }
user.setUsername(email); user.setUsername(email);
} }

View file

@ -302,6 +302,7 @@ public class RealmAdminResource {
} }
} }
boolean wasDuplicateEmailsAllowed = realm.isDuplicateEmailsAllowed();
RepresentationToModel.updateRealm(rep, realm, session); RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders // Refresh periodic sync tasks for configured federationProviders
@ -312,6 +313,12 @@ public class RealmAdminResource {
} }
adminEvent.operation(OperationType.UPDATE).representation(StripSecretsUtils.strip(rep)).success(); adminEvent.operation(OperationType.UPDATE).representation(StripSecretsUtils.strip(rep)).success();
if (rep.isDuplicateEmailsAllowed() != null && rep.isDuplicateEmailsAllowed() != wasDuplicateEmailsAllowed) {
UserCache cache = session.getProvider(UserCache.class);
if (cache != null) cache.clear();
}
return Response.noContent().build(); return Response.noContent().build();
} catch (PatternSyntaxException e) { } catch (PatternSyntaxException e) {
return ErrorResponse.error("Specified regex pattern(s) is invalid.", Response.Status.BAD_REQUEST); return ErrorResponse.error("Specified regex pattern(s) is invalid.", Response.Status.BAD_REQUEST);

View file

@ -208,7 +208,7 @@ public class UsersResource {
if (session.users().getUserByUsername(rep.getUsername(), realm) != null) { if (session.users().getUserByUsername(rep.getUsername(), realm) != null) {
return ErrorResponse.exists("User exists with same username"); return ErrorResponse.exists("User exists with same username");
} }
if (rep.getEmail() != null && session.users().getUserByEmail(rep.getEmail(), realm) != null) { if (rep.getEmail() != null && !realm.isDuplicateEmailsAllowed() && session.users().getUserByEmail(rep.getEmail(), realm) != null) {
return ErrorResponse.exists("User exists with same email"); return ErrorResponse.exists("User exists with same email");
} }

View file

@ -549,6 +549,12 @@ public class AccountTest extends TestRealmKeycloakTest {
testRealm().update(testRealm); testRealm().update(testRealm);
} }
private void setDuplicateEmailsAllowed(boolean allowed) {
RealmRepresentation testRealm = testRealm().toRepresentation();
testRealm.setDuplicateEmailsAllowed(allowed);
testRealm().update(testRealm);
}
@Test @Test
public void changeUsername() { public void changeUsername() {
// allow to edit the username in realm // allow to edit the username in realm
@ -659,7 +665,7 @@ public class AccountTest extends TestRealmKeycloakTest {
// KEYCLOAK-1534 // KEYCLOAK-1534
@Test @Test
public void changeEmailToExisting() { public void changeEmailToExistingForbidden() {
profilePage.open(); profilePage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
@ -693,6 +699,24 @@ public class AccountTest extends TestRealmKeycloakTest {
profilePage.updateProfile("Tom", "Brady", "test-user@localhost"); profilePage.updateProfile("Tom", "Brady", "test-user@localhost");
events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); events.expectAccount(EventType.UPDATE_PROFILE).assertEvent();
} }
@Test
public void changeEmailToExistingAllowed() {
setDuplicateEmailsAllowed(true);
profilePage.open();
loginPage.login("test-user@localhost", "password");
events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
Assert.assertEquals("test-user@localhost", profilePage.getUsername());
Assert.assertEquals("test-user@localhost", profilePage.getEmail());
// Change to the email, which some other user has
profilePage.updateProfile("New first", "New last", "test-user-no-access@localhost");
Assert.assertEquals("Your account has been updated.", profilePage.getSuccess());
}
@Test @Test
public void setupTotp() { public void setupTotp() {

View file

@ -87,6 +87,7 @@ public class PartialImportTest extends AbstractAuthTest {
public void initAdminEvents() { public void initAdminEvents() {
RealmRepresentation realmRep = RealmBuilder.edit(testRealmResource().toRepresentation()).testEventListener().build(); RealmRepresentation realmRep = RealmBuilder.edit(testRealmResource().toRepresentation()).testEventListener().build();
realmId = realmRep.getId(); realmId = realmRep.getId();
realmRep.setDuplicateEmailsAllowed(false);
adminClient.realm(realmRep.getRealm()).update(realmRep); adminClient.realm(realmRep.getRealm()).update(realmRep);
piRep = new PartialImportRepresentation(); piRep = new PartialImportRepresentation();
@ -321,6 +322,40 @@ public class PartialImportTest extends AbstractAuthTest {
} }
} }
@Test
public void testAddUsersWithDuplicateEmailsForbidden() {
assertAdminEvents.clear();
setFail();
addUsers();
UserRepresentation user = createUserRepresentation(USER_PREFIX + 999, USER_PREFIX + 1 + "@foo.com", "foo", "bar", true);
piRep.getUsers().add(user);
Response response = testRealmResource().partialImport(piRep);
assertEquals(409, response.getStatus());
}
@Test
public void testAddUsersWithDuplicateEmailsAllowed() {
RealmRepresentation realmRep = new RealmRepresentation();
realmRep.setDuplicateEmailsAllowed(true);
adminClient.realm(realmId).update(realmRep);
assertAdminEvents.clear();
setFail();
addUsers();
doImport();
UserRepresentation user = createUserRepresentation(USER_PREFIX + 999, USER_PREFIX + 1 + "@foo.com", "foo", "bar", true);
piRep.setUsers(Arrays.asList(user));
PartialImportResults results = doImport();
assertEquals(1, results.getAdded());
}
@Test @Test
public void testAddUsersWithTermsAndConditions() { public void testAddUsersWithTermsAndConditions() {
assertAdminEvents.clear(); assertAdminEvents.clear();

View file

@ -412,6 +412,8 @@ public class RealmTest extends AbstractAdminTest {
if (realm.isRegistrationEmailAsUsername() != null) assertEquals(realm.isRegistrationEmailAsUsername(), storedRealm.isRegistrationEmailAsUsername()); if (realm.isRegistrationEmailAsUsername() != null) assertEquals(realm.isRegistrationEmailAsUsername(), storedRealm.isRegistrationEmailAsUsername());
if (realm.isRememberMe() != null) assertEquals(realm.isRememberMe(), storedRealm.isRememberMe()); if (realm.isRememberMe() != null) assertEquals(realm.isRememberMe(), storedRealm.isRememberMe());
if (realm.isVerifyEmail() != null) assertEquals(realm.isVerifyEmail(), storedRealm.isVerifyEmail()); if (realm.isVerifyEmail() != null) assertEquals(realm.isVerifyEmail(), storedRealm.isVerifyEmail());
if (realm.isLoginWithEmailAllowed() != null) assertEquals(realm.isLoginWithEmailAllowed(), storedRealm.isLoginWithEmailAllowed());
if (realm.isDuplicateEmailsAllowed() != null) assertEquals(realm.isDuplicateEmailsAllowed(), storedRealm.isDuplicateEmailsAllowed());
if (realm.isResetPasswordAllowed() != null) assertEquals(realm.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed()); if (realm.isResetPasswordAllowed() != null) assertEquals(realm.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed());
if (realm.isEditUsernameAllowed() != null) assertEquals(realm.isEditUsernameAllowed(), storedRealm.isEditUsernameAllowed()); if (realm.isEditUsernameAllowed() != null) assertEquals(realm.isEditUsernameAllowed(), storedRealm.isEditUsernameAllowed());
if (realm.getSslRequired() != null) assertEquals(realm.getSslRequired(), storedRealm.getSslRequired()); if (realm.getSslRequired() != null) assertEquals(realm.getSslRequired(), storedRealm.getSslRequired());

View file

@ -99,9 +99,9 @@ public class ExportImportTest extends AbstractExportImportTest {
testRealmExportImport(); testRealmExportImport();
// There should be 3 files in target directory (1 realm, 3 user) // There should be 3 files in target directory (1 realm, 4 user)
File[] files = new File(targetDirPath).listFiles(); File[] files = new File(targetDirPath).listFiles();
assertEquals(4, files.length); assertEquals(5, files.length);
} }
@Test @Test

View file

@ -62,12 +62,12 @@ public class RegisterTest extends TestRealmKeycloakTest {
} }
@Test @Test
public void registerExistingUser() { public void registerExistingUsernameForbidden() {
loginPage.open(); loginPage.open();
loginPage.clickRegister(); loginPage.clickRegister();
registerPage.assertCurrent(); registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "registerExistingUser@email", "test-user@localhost", "password", "password"); registerPage.register("firstName", "lastName", "registerExistingUser@email", "roleRichUser", "password", "password");
registerPage.assertCurrent(); registerPage.assertCurrent();
assertEquals("Username already exists.", registerPage.getError()); assertEquals("Username already exists.", registerPage.getError());
@ -80,10 +80,57 @@ public class RegisterTest extends TestRealmKeycloakTest {
assertEquals("", registerPage.getPassword()); assertEquals("", registerPage.getPassword());
assertEquals("", registerPage.getPasswordConfirm()); assertEquals("", registerPage.getPasswordConfirm());
events.expectRegister("test-user@localhost", "registerExistingUser@email") events.expectRegister("roleRichUser", "registerExistingUser@email")
.removeDetail(Details.EMAIL) .removeDetail(Details.EMAIL)
.user((String) null).error("username_in_use").assertEvent(); .user((String) null).error("username_in_use").assertEvent();
} }
@Test
public void registerExistingEmailForbidden() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "test-user@localhost", "registerExistingUser", "password", "password");
registerPage.assertCurrent();
assertEquals("Email already exists.", registerPage.getError());
// assert form keeps form fields on error
assertEquals("firstName", registerPage.getFirstName());
assertEquals("lastName", registerPage.getLastName());
assertEquals("", registerPage.getEmail());
assertEquals("registerExistingUser", registerPage.getUsername());
assertEquals("", registerPage.getPassword());
assertEquals("", registerPage.getPasswordConfirm());
events.expectRegister("registerExistingUser", "registerExistingUser@email")
.removeDetail(Details.EMAIL)
.user((String) null).error("email_in_use").assertEvent();
}
@Test
public void registerExistingEmailAllowed() {
setDuplicateEmailsAllowed(true);
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "test-user@localhost", "registerExistingEmailUser", "password", "password");
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerExistingEmailUser", "test-user@localhost").assertEvent().getUserId();
events.expectLogin().detail("username", "registerexistingemailuser").user(userId).assertEvent();
UserRepresentation user = getUser(userId);
Assert.assertNotNull(user);
assertEquals("registerexistingemailuser", user.getUsername());
assertEquals("test-user@localhost", user.getEmail());
assertEquals("firstName", user.getFirstName());
assertEquals("lastName", user.getLastName());
}
@Test @Test
public void registerUserInvalidPasswordConfirm() { public void registerUserInvalidPasswordConfirm() {
@ -397,5 +444,11 @@ public class RegisterTest extends TestRealmKeycloakTest {
realm.setRegistrationEmailAsUsername(value); realm.setRegistrationEmailAsUsername(value);
testRealm().update(realm); testRealm().update(realm);
} }
private void setDuplicateEmailsAllowed(boolean allowed) {
RealmRepresentation testRealm = testRealm().toRepresentation();
testRealm.setDuplicateEmailsAllowed(allowed);
testRealm().update(testRealm);
}
} }

View file

@ -0,0 +1,100 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
/**
* @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
*/
public class AccessTokenDuplicateEmailsNotCleanedUpTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Before
public void clientConfiguration() {
oauth.clientId("test-app");
oauth.realm("test-duplicate-emails");
RealmRepresentation realmRep = new RealmRepresentation();
// change realm settings to allow login with email after having imported users with duplicate email addresses
realmRep.setLoginWithEmailAllowed(true);
adminClient.realm("test-duplicate-emails").update(realmRep);
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm-duplicate-emails.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void loginWithNonDuplicateEmail() throws Exception {
oauth.doLogin("non-duplicate-email-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "non-duplicate-email-user").getId(), token.getSubject());
}
@Test
public void loginWithDuplicateEmail() throws Exception {
oauth.doLogin("duplicate-email-user@localhost", "password");
assertEquals("Username already exists.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
}
@Test
public void loginWithUserHavingDuplicateEmailByUsername() throws Exception {
oauth.doLogin("duplicate-email-user1", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user1").getId(), token.getSubject());
assertEquals("duplicate-email-user@localhost", token.getEmail());
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.openqa.selenium.By;
/**
* @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
*/
public class AccessTokenDuplicateEmailsTest extends AbstractKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Before
public void clientConfiguration() {
oauth.clientId("test-app");
oauth.realm("test-duplicate-emails");
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm-duplicate-emails.json"), RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void loginFormUsernameLabel() throws Exception {
oauth.openLoginForm();
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/does/not/matter/");
assertEquals("Username", driver.findElement(By.xpath("//label[@for='username']")).getText());
}
@Test
public void loginWithNonDuplicateEmailUser() throws Exception {
oauth.doLogin("non-duplicate-email-user", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "non-duplicate-email-user").getId(), token.getSubject());
assertEquals("non-duplicate-email-user@localhost", token.getEmail());
}
@Test
public void loginWithFirstDuplicateEmailUser() throws Exception {
oauth.doLogin("duplicate-email-user1", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user1").getId(), token.getSubject());
assertEquals("duplicate-email-user@localhost", token.getEmail());
}
@Test
public void loginWithSecondDuplicateEmailUser() throws Exception {
oauth.doLogin("duplicate-email-user2", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test-duplicate-emails"), "duplicate-email-user2").getId(), token.getSubject());
assertEquals("duplicate-email-user@localhost", token.getEmail());
}
@Test
public void loginWithNonDuplicateEmail() throws Exception {
oauth.doLogin("non-duplicate-email-user@localhost", "password");
assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
}
@Test
public void loginWithDuplicateEmail() throws Exception {
oauth.doLogin("duplicate-email-user@localhost", "password");
assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
/**
* @author <a href="mailto:slawomir@dabek.name">Slawomir Dabek</a>
*/
public class AccessTokenNoEmailLoginTest extends AbstractKeycloakTest {
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Before
public void clientConfiguration() {
oauth.clientId("test-app");
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
realm.setLoginWithEmailAllowed(false);
testRealms.add(realm);
}
@Test
public void loginFormUsernameLabel() throws Exception {
oauth.openLoginForm();
assertEquals("Username", driver.findElement(By.xpath("//label[@for='username']")).getText());
}
@Test
public void loginWithUsername() throws Exception {
oauth.doLogin("non-duplicate-email-user", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
assertEquals(200, response.getStatusCode());
AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test"), "non-duplicate-email-user").getId(), token.getSubject());
assertEquals("non-duplicate-email-user@localhost", token.getEmail());
}
@Test
public void loginWithEmail() throws Exception {
oauth.doLoginGrant("non-duplicate-email-user@localhost", "password");
assertEquals("Invalid username or password.", driver.findElement(By.xpath("//span[@class='kc-feedback-text']")).getText());
}
}

View file

@ -93,6 +93,7 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT; import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper; import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
import org.openqa.selenium.By;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -135,6 +136,13 @@ public class AccessTokenTest extends AbstractKeycloakTest {
testRealms.add(realm); testRealms.add(realm);
} }
@Test
public void loginFormUsernameOrEmailLabel() throws Exception {
oauth.openLoginForm();
assertEquals("Username or email", driver.findElement(By.xpath("//label[@for='username']")).getText());
}
@Test @Test
public void accessTokenRequest() throws Exception { public void accessTokenRequest() throws Exception {

View file

@ -0,0 +1,142 @@
{
"id": "test-duplicate-emails",
"realm": "test-duplicate-emails",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"resetPasswordAllowed": true,
"editUsernameAllowed" : true,
"loginWithEmailAllowed": false,
"duplicateEmailsAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",
"port":"3025"
},
"users" : [
{
"username" : "non-duplicate-email-user",
"enabled": true,
"email" : "non-duplicate-email-user@localhost",
"firstName": "Brian",
"lastName": "Cohen",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
},
{
"username" : "duplicate-email-user1",
"enabled": true,
"email" : "duplicate-email-user@localhost",
"firstName": "Agent",
"lastName": "Smith",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
},
{
"username" : "duplicate-email-user2",
"enabled": true,
"email" : "duplicate-email-user@localhost",
"firstName": "Agent",
"lastName": "Smith",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
}
],
"scopeMappings": [
{
"client": "test-app",
"roles": ["user"]
}
],
"clients": [
{
"clientId": "test-app",
"enabled": true,
"baseUrl": "http://localhost:8180/auth/realms/master/app/auth",
"redirectUris": [
"http://localhost:8180/auth/realms/master/app/auth/*"
],
"adminUrl": "http://localhost:8180/auth/realms/master/app/admin",
"secret": "password"
}
],
"roles" : {
"realm" : [
{
"name": "user",
"description": "Have User privileges"
},
{
"name": "admin",
"description": "Have Administrator privileges"
},
{
"name": "customer-user-premium",
"description": "Have User Premium privileges"
},
{
"name": "sample-realm-role",
"description": "Sample realm role"
}
],
"client" : {
"test-app" : [
{
"name": "customer-user",
"description": "Have Customer User privileges"
},
{
"name": "customer-admin",
"description": "Have Customer Admin privileges"
},
{
"name": "sample-client-role",
"description": "Sample client role"
},
{
"name": "customer-admin-composite-role",
"description": "Have Customer Admin privileges via composite role",
"composite" : true,
"composites" : {
"realm" : [ "customer-user-premium" ],
"client" : {
"test-app" : [ "customer-admin" ]
}
}
}
]
}
},
"groups" : [],
"clientScopeMappings": {},
"internationalizationEnabled": true,
"supportedLocales": ["en", "de"],
"defaultLocale": "en",
"eventsListeners": ["jboss-logging", "event-queue"]
}

View file

@ -100,6 +100,22 @@
"clientRoles": { "clientRoles": {
"test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ] "test-app-scope": [ "test-app-allowed-by-scope", "test-app-disallowed-by-scope" ]
} }
},
{
"username" : "non-duplicate-email-user",
"enabled": true,
"email" : "non-duplicate-email-user@localhost",
"firstName": "Brian",
"lastName": "Cohen",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user", "offline_access"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
} }
], ],
"scopeMappings": [ "scopeMappings": [

View file

@ -32,6 +32,10 @@ resetPasswordAllowed=Forgot password
resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials. resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials.
rememberMe=Remember Me rememberMe=Remember Me
rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.
loginWithEmailAllowed=Login with email
loginWithEmailAllowed.tooltip=Allow users to log in with their email address.
duplicateEmailsAllowed=Duplicate emails
duplicateEmailsAllowed.tooltip=Allow multiple users to have the same email address. Changing this setting will also clear the users cache. It is recommended to manually update email constraints of existing users in the database after switching off support for duplicate email addresses.
verifyEmail=Verify email verifyEmail=Verify email
verifyEmail.tooltip=Require the user to verify their email address the first time they login. verifyEmail.tooltip=Require the user to verify their email address the first time they login.
sslRequired=Require SSL sslRequired=Require SSL

View file

@ -45,6 +45,20 @@
</div> </div>
<kc-tooltip>{{:: 'verifyEmail.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'verifyEmail.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group">
<label for="loginWithEmailAllowed" class="col-md-2 control-label">{{:: 'loginWithEmailAllowed' | translate}}</label>
<div class="col-md-6">
<input ng-model="realm.loginWithEmailAllowed" name="loginWithEmailAllowed" id="loginWithEmailAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'loginWithEmailAllowed.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" ng-show="!realm.loginWithEmailAllowed && !realm.registrationEmailAsUsername">
<label for="duplicateEmailsAllowed" class="col-md-2 control-label">{{:: 'duplicateEmailsAllowed' | translate}}</label>
<div class="col-md-6">
<input ng-model="realm.duplicateEmailsAllowed" name="duplicateEmailsAllowed" id="duplicateEmailsAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'duplicateEmailsAllowed.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label for="sslRequired" class="col-md-2 control-label">{{:: 'sslRequired' | translate}}</label> <label for="sslRequired" class="col-md-2 control-label">{{:: 'sslRequired' | translate}}</label>
<div class="col-md-2"> <div class="col-md-2">

View file

@ -8,7 +8,7 @@
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label> <label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus/> <input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus/>

View file

@ -9,7 +9,7 @@
<form id="kc-form-login" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-form-login" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label> <label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">