Microsoft ASP.NET ID - Multiple Users with the Same Name

I'm trying to do something quite exotic, I believe, and run into a few problems that I hope can be solved with the help of users here on StackOverflow.

History

I am writing and an application that requires authentication and registration. I decided to use Microsoft.AspNet.Identity . I have not used it a lot in the past, so do not judge me by this decision.

The structure above contains a user table in which all registered users are stored.

I created a sample image to show how the application will work.

enter image description here

The application consists of three different components:

  • Backend (WebAPI).
  • Clients (directly using WebAPI).
  • End users (using a mobile application - iOS).

So, I have a backend where clients can register. There is one unique user for each member, so there is no problem. The customer is the company here, and End users are the clicks of the company.

You may already see the problem. It is entirely possible that User 1 is a customer in Customer 1 , but also in Customer 2 .

Now the client can invite the participant to use the mobile application. When the client does this, the end user receives an email with a link for the active user.

Now everything works fine, as long as your users are unique, but I have a user who is a customer of Customer 1 and Customer 2 . Both clients can invite the same user, and the user will need to register 2 times, one for each Customer .

Problem

Within Microsoft.AspNet.Identity users must be unique, which, according to my situation, I can’t manage.

Question

Can I add additional parameters to IdentityUser to make sure the user is unique?

What i have already done

  • Create your own class that inherits from IdentityUser and which includes the application identifier:

     public class AppServerUser : IdentityUser { #region Properties /// <summary> /// Gets or sets the id of the member that this user belongs to. /// </summary> public int MemberId { get; set; } #endregion } 
  • Changed my IDbContext accordingly:

     public class AppServerContext : IdentityDbContext<AppServerUser>, IDbContext { } 
  • Modified calls that use the framework.

     IUserStore<IdentityUser> -> IUserStore<AppServerUser> UserManager<IdentityUser>(_userStore) -> UserManager<AppServerUser>(_userStore); 

    _userStore disconnected from type IUserStore<AppServerUser>

However, when I register a user with an already accepted username, I still get an error message stating that the username has already been executed:

 var result = await userManager.CreateAsync(new AppServerUser {UserName = "testing"}, "testing"); 

What I believe is a solution

I really believe that I need to change the UserManager , but I'm not sure about that. I hope someone here has enough knowledge about the structure to help me, because it really blocks the development of our application.

If this is not possible, I would also like to know, and perhaps you can point me to another structure that allows me to do this.

Note. . I do not want to write all user management myself, because it will overwrite the wheel.

+5
source share
2 answers

First of all, I understand the idea of ​​your thoughts, and therefore I will begin to explain why you cannot create multiple users with the same name.

Username with the same name: The problem you are facing right now is with IdentityDbContext. As you can see ( https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs ), identityDbContext sets the rules for unique users and roles: first, when creating the model:

 protected override void OnModelCreating(DbModelBuilder modelBuilder) { if (modelBuilder == null) { throw new ArgumentNullException("modelBuilder"); } // Needed to ensure subclasses share the same table var user = modelBuilder.Entity<TUser>() .ToTable("AspNetUsers"); user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId); user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId); user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId); user.Property(u => u.UserName) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true })); // CONSIDER: u.Email is Required if set on options? user.Property(u => u.Email).HasMaxLength(256); modelBuilder.Entity<TUserRole>() .HasKey(r => new { r.UserId, r.RoleId }) .ToTable("AspNetUserRoles"); modelBuilder.Entity<TUserLogin>() .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId }) .ToTable("AspNetUserLogins"); modelBuilder.Entity<TUserClaim>() .ToTable("AspNetUserClaims"); var role = modelBuilder.Entity<TRole>() .ToTable("AspNetRoles"); role.Property(r => r.Name) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true })); role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId); } 

secondly on the checked object:

 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; //check for uniqueness of user name and email if (user != null) { if (Users.Any(u => String.Equals(u.UserName, user.UserName))) { errors.Add(new DbValidationError("User", String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateUserName, user.UserName))); } if (RequireUniqueEmail && Users.Any(u => String.Equals(u.Email, user.Email))) { errors.Add(new DbValidationError("User", String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateEmail, user.Email))); } } else { var role = entityEntry.Entity as TRole; //check for uniqueness of role name if (role != null && Roles.Any(r => String.Equals(r.Name, role.Name))) { errors.Add(new DbValidationError("Role", String.Format(CultureInfo.CurrentCulture, IdentityResources.RoleAlreadyExists, role.Name))); } } if (errors.Any()) { return new DbEntityValidationResult(entityEntry, errors); } } return base.ValidateEntity(entityEntry, items); } } 

Tip: What you can do to overcome this problem is easy: in the ApplicationDbContext that you have, override both of these methods to overcome this check

Caution Without this check, you can now use multiple users with the same name, but you must implement rules that prevent you from creating users in the same client with the same username. What you can do by adding this as a confirmation.

Hope the help was valuable :) Greetings!

0
source

Maybe someone might find this helpful. In our project, we use the ASP.NET 2 identifier, and once we came across a case where two users have the same name. We use emails as logins in our application, and they really need to be unique. But we do not want usernames to be unique. We just set up several classes of identification system as follows:

  • Changed our AppIdentityDbContext , creating an index in the UserName field as non-unique and overriding ValidateEntity complicated way. And then using the migration update database. The code looks like this:

     public class AppIdentityDbContext : IdentityDbContext<AppUser> { public AppIdentityDbContext() : base("IdentityContext", throwIfV1Schema: false) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // This needs to go before the other rules! *****[skipped some other code]***** // In order to support multiple user names // I replaced unique index of UserNameIndex to non-unique modelBuilder .Entity<AppUser>() .Property(c => c.UserName) .HasColumnAnnotation( "Index", new IndexAnnotation( new IndexAttribute("UserNameIndex") { IsUnique = false })); modelBuilder .Entity<AppUser>() .Property(c => c.Email) .IsRequired() .HasColumnAnnotation( "Index", new IndexAnnotation(new[] { new IndexAttribute("EmailIndex") {IsUnique = true} })); } /// <summary> /// Override 'ValidateEntity' to support multiple users with the same name /// </summary> /// <param name="entityEntry"></param> /// <param name="items"></param> /// <returns></returns> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { // call validate and check results var result = base.ValidateEntity(entityEntry, items); if (result.ValidationErrors.Any(err => err.PropertyName.Equals("User"))) { // Yes I know! Next code looks not good, because I rely on internal messages of Identity 2, but I should track here only error message instead of rewriting the whole IdentityDbContext var duplicateUserNameError = result.ValidationErrors .FirstOrDefault( err => Regex.IsMatch( err.ErrorMessage, @"Name\s+(.+)is\s+already\s+taken", RegexOptions.IgnoreCase)); if (null != duplicateUserNameError) { result.ValidationErrors.Remove(duplicateUserNameError); } } return result; } } 
  • Create your own IIdentityValidator<AppUser> interface class and set it to our UserManager<AppUser>.UserValidator :

     public class AppUserValidator : IIdentityValidator<AppUser> { /// <summary> /// Constructor /// </summary> /// <param name="manager"></param> public AppUserValidator(UserManager<AppUser> manager) { Manager = manager; } private UserManager<AppUser, string> Manager { get; set; } /// <summary> /// Validates a user before saving /// </summary> /// <param name="item"></param> /// <returns></returns> public virtual async Task<IdentityResult> ValidateAsync(AppUser item) { if (item == null) { throw new ArgumentNullException("item"); } var errors = new List<string>(); ValidateUserName(item, errors); await ValidateEmailAsync(item, errors); if (errors.Count > 0) { return IdentityResult.Failed(errors.ToArray()); } return IdentityResult.Success; } private void ValidateUserName(AppUser user, List<string> errors) { if (string.IsNullOrWhiteSpace(user.UserName)) { errors.Add("Name cannot be null or empty."); } else if (!Regex.IsMatch(user.UserName, @"^[ A-Za-z0-9@ _\.]+$")) { // If any characters are not letters or digits, its an illegal user name errors.Add(string.Format("User name {0} is invalid, can only contain letters or digits.", user.UserName)); } } // make sure email is not empty, valid, and unique private async Task ValidateEmailAsync(AppUser user, List<string> errors) { var email = user.Email; if (string.IsNullOrWhiteSpace(email)) { errors.Add(string.Format("{0} cannot be null or empty.", "Email")); return; } try { var m = new MailAddress(email); } catch (FormatException) { errors.Add(string.Format("Email '{0}' is invalid", email)); return; } var owner = await Manager.FindByEmailAsync(email); if (owner != null && !owner.Id.Equals(user.Id)) { errors.Add(string.Format(CultureInfo.CurrentCulture, "Email '{0}' is already taken.", email)); } } } public class AppUserManager : UserManager<AppUser> { public AppUserManager( IUserStore<AppUser> store, IDataProtectionProvider dataProtectionProvider, IIdentityMessageService emailService) : base(store) { // Configure validation logic for usernames UserValidator = new AppUserValidator(this); 
  • And the last step is to change the AppSignInManager . Since now our usernames are not unique, we use email to log in:

     public class AppSignInManager : SignInManager<AppUser, string> { .... public virtual async Task<SignInStatus> PasswordSignInViaEmailAsync(string userEmail, string password, bool isPersistent, bool shouldLockout) { var userManager = ((AppUserManager) UserManager); if (userManager == null) { return SignInStatus.Failure; } var user = await UserManager.FindByEmailAsync(userEmail); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.CheckPasswordAsync(user, password)) { await UserManager.ResetAccessFailedCountAsync(user.Id); await SignInAsync(user, isPersistent, false); return SignInStatus.Success; } if (shouldLockout) { // If lockout is requested, increment access failed count which might lock out the user await UserManager.AccessFailedAsync(user.Id); if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } } return SignInStatus.Failure; } 

    And now the code is as follows:

     [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Index(User model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } var result = await signInManager.PasswordSignInViaEmailAsync( model.Email, model.Password, model.StaySignedIn, true); var errorMessage = string.Empty; switch (result) { case SignInStatus.Success: if (IsLocalValidUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); case SignInStatus.Failure: errorMessage = Messages.LoginController_Index_AuthorizationError; break; case SignInStatus.LockedOut: errorMessage = Messages.LoginController_Index_LockoutError; break; case SignInStatus.RequiresVerification: throw new NotImplementedException(); } ModelState.AddModelError(string.Empty, errorMessage); return View(model); } 

PS I don't really like the way I override the ValidateEntity method. But I decided to do this because instead I have to implement a DbContext class that is almost identical to IdentityDbContext , so I need to track changes on it when the ID service pack is in my project.

+2
source

All Articles