Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to allow user to register with duplicate UserName using Identity Framework 1.0

I want to develop an application in MVC using Identity Framework 1.0 which allow users to register with same username used by some other user.

When deleting a user I want to set its IsDeleted custom property to true rather than deleting the user from database. In this case another user can use the UserName of the user whose IsDeleted is set to true.

But the default UserManager.CreateAsync(user, password); method is preventing doing this.

I had overridden the ValidateEntity method of IdentityDbContext like this

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    if ((entityEntry != null) && (entityEntry.State == EntityState.Added))
    {
        ApplicationUser user = entityEntry.Entity as ApplicationUser;
        if ((user != null) && this.Users.Any<ApplicationUser>(u => string.Equals(u.UserName, user.UserName) && u.IsDeleted==false))
        {
            return new DbEntityValidationResult(entityEntry, new List<DbValidationError>()) {
                ValidationErrors = { 
                    new DbValidationError("User", string.Format(CultureInfo.CurrentCulture, 
                       "", new object[] { user.UserName }))
                } 
            };
        }
        IdentityRole role = entityEntry.Entity as IdentityRole;
        if ((role != null) && this.Roles.Any<IdentityRole>(r => string.Equals(r.Name, role.Name)))
        {
            return new DbEntityValidationResult(entityEntry, new List<DbValidationError>()) { 
                ValidationErrors = { 
                    new DbValidationError("Role", string.Format(CultureInfo.CurrentCulture, 
                        "", new object[] { role.Name })) } };
        }
    }
    return base.ValidateEntity(entityEntry, items);
}

Here is my register method where user is created

public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName, Email = model.Email.ToLower(), CreatedBy = model.UserName, CreatedDate = DateTime.UtcNow, };

        user.ConfirmedEmail = false;
        var result = await _accountService.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {

            TempData["MessageConfirm"] = user.Email;
            return RedirectToAction("Confirm", "Account");
        }
        else
        {
            AddErrors(result);
        }
    }
    // If we got this far, something failed, redisplay form
    return View(model);
}

ValidateEntity method should be executed when await _accountService.CreateAsync(user, model.Password); executes. But it is executing after the register method completes it's execution. So the result throws error.

Any suggestions how I can achieve this?

like image 853
Twix Avatar asked Mar 17 '15 08:03

Twix


People also ask

How do I add Owin to an existing project?

In Solution Explorer, right-click your project, select Add, and then Add New Item. In the search text box dialog, type "owin". Name the class "Startup" and select Add.

What is identity framework in asp net?

ASP.NET Core Identity provides a framework for managing and storing user accounts in ASP.NET Core apps. Identity is added to your project when Individual User Accounts is selected as the authentication mechanism. By default, Identity makes use of an Entity Framework (EF) Core data model.

How do you create an identity user?

To create new Users in ASP.NET Core Identity we will need a C# Class. So create a new class called User. cs inside the Models folders. Next, add 3 public properties to it, which are Name, Email & Password, each of type string.


1 Answers

My pet peeve is when people decide to give advice in lieu of answering the question asked. usernames don't have to be unique. The db key is userid not user name. If you have application that services several companies there is a chance that employees of different companies will have the same username. In order to do this you have to extent aspnet identity.

https://www.scottbrady91.com/ASPNET-Identity/Quick-and-Easy-ASPNET-Identity-Multitenancy

IdentityUser To start with we’ll need to add the claim of TenantId (you can rename this as fits your business requirements) by extending the IdentityUser class. Whilst this is conceptually a claim, we will take advantage of the AspNetUser table and add TenantId as a property, as we will be querying by this property a fair bit. For simplicity I have added the TenantId as an int however a non-iterative alternative would be to use a string.

    public class ApplicationUser : IdentityUser {
    public int TenantId { get; set; }
}

UserStore Next we’ll implement the UserStore for our new user that is aware of our new property. Here we are using a property within our UserStore class to set our TenantId, allowing us to override the base implementation with our multi-tenanted implementation.

    public class ApplicationUserStore<TUser> : UserStore<TUser> 
  where TUser : ApplicationUser {
    public ApplicationUserStore(DbContext context)
      : base(context) {
    }

    public int TenantId { get; set; }
}

CreateUserAsync

public override Task CreateAsync(TUser user) {
if (user == null) {
    throw new ArgumentNullException("user");
}

user.TenantId = this.TenantId;
return base.CreateAsync(user);

}

FindByEmailAsync

public override Task<TUser> FindByEmailAsync(string email) {
return this.GetUserAggregateAsync(u => u.Email.ToUpper() == email.ToUpper() 
    && u.TenantId == this.TenantId);

}

FindByNameAsync

public override Task<TUser> FindByNameAsync(string userName) {
return this.GetUserAggregateAsync(u => u.UserName.ToUpper() == userName.ToUpper() 
    && u.TenantId == this.TenantId);

}

UserValidator Whilst the default UserValidator has hardcoded checks for duplicate user names, our new implementation of the UserStore methods FindByNameAsync and FindByEmailAsync will allow for the correct multi-tenanted behaviour (assuming you have set a TenantId within the UserStore). This means we can take full advantage of the default UserValidator and extend it if necessary.

IdentityDbContext Now here’s an awkward bit. The ASP.NET Identity team have again hardcoded a check for duplicate usernames within the IdentityDbContext class, however this time it is both within the ValidateEntity method and in the EF database schema itself using an index.

The index can be solved by extending the OnModelCreating method to change the unique index based on username to also look for our TenantId (a composite index). This saves us losing this useful index and optimises our database for multitenancy. You can do this with the following override method:

        public class ApplicationUserDbContext<TUser> : IdentityDbContext<TUser> 
      where TUser : ApplicationUser {
        public ApplicationUserDbContext(string nameOrConnectionString)
          : base(nameOrConnectionString) {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder) {
            base.OnModelCreating(modelBuilder);

            var user = modelBuilder.Entity<TUser>();

            user.Property(u => u.UserName)
                .IsRequired()
                .HasMaxLength(256)
                .HasColumnAnnotation("Index", new IndexAnnotation(
                    new IndexAttribute("UserNameIndex") { IsUnique = true, Order = 1}));

            user.Property(u => u.TenantId)
                .IsRequired()
                .HasColumnAnnotation("Index", new IndexAnnotation(
                    new IndexAttribute("UserNameIndex") { IsUnique = true, Order = 2 }));
        }
    }

The ValidateEntity method is a bit more tricky however, as we will have to reimplement the entire method in order to remove the hardcoded username checks:

        protected override DbEntityValidationResult ValidateEntity(
          DbEntityEntry entityEntry, IDictionary<object, object> items) {
            if (entityEntry != null && entityEntry.State == EntityState.Added) {
                var errors = new List<DbValidationError>();
                var user = entityEntry.Entity as TUser;

                if (user != null) {
                    if (this.Users.Any(u => string.Equals(u.UserName, user.UserName) 
                      && u.TenantId == user.TenantId)) {
                        errors.Add(new DbValidationError("User", 
                          string.Format("Username {0} is already taken for AppId {1}", 
                            user.UserName, user.TenantId)));
                    }

                    if (this.RequireUniqueEmail 
                      && this.Users.Any(u => string.Equals(u.Email, user.Email) 
                      && u.TenantId == user.TenantId)) {
                        errors.Add(new DbValidationError("User", 
                          string.Format("Email Address {0} is already taken for AppId {1}", 
                            user.UserName, user.TenantId)));
                    }
                }
                else {
                    var role = entityEntry.Entity as IdentityRole;

                    if (role != null && this.Roles.Any(r => string.Equals(r.Name, role.Name))) {
                        errors.Add(new DbValidationError("Role", 
                          string.Format("Role {0} already exists", role.Name)));
                    }
                }
                if (errors.Any()) {
                    return new DbEntityValidationResult(entityEntry, errors);
                }
            }

            return new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
        }

Client All that remains now is to initialise the classes. Don't forget you will need to supply the TenantId each time you new up the context. See the below example (note the use of 'example', these classes are all disposable...).

    var context = new ApplicationUserDbContext<ApplicationUser>("DefaultConnection");
var userStore = new ApplicationUserStore<ApplicationUser>(context) { TenantId = 1 };
var userManager = new UserManager<ApplicationUser, string>(userStore);
like image 118
Tony Cobb Avatar answered Oct 10 '22 05:10

Tony Cobb