-1

Current project:

I am experiencing a very strange issue with a migrated database. The old database could not be salvaged (wrong design, badly entered data, the works), so a completely new system was designed and built for the customer.

Due to the bad state of the data, migration had to be punted through Excel, so that the data could be cleaned up and some complex calculations and validation done to certain fields.

I re-imported the data in such a way that for the users, their PasswordHash and SecurityStamp fields were null values (the default setting). I then ran a headless script in the browser (an ActionResult that simply redirected back to /Home/Index once it was done) that went through all users and assigned them a random GUID as a password. The intent was that a password reset eMail would be sent out for everyone.

Unfortunately, for 102 users out of over 2,000, this headless script was unable to update the user. As in, the PasswordHash and SecurityStamp remained null.

This problem has now extended to the password reset eMail that was sent out. The link in each eMail works, in that it is able to reset 95% of all accounts, but for these 102 accounts it is utterly unable to clear, set or reset the password. I have tried to debug this, but the debugger does not dive into UserManager (understandable), and so I cannot see where and how it fails.

Since 95% of all accounts are functioning just fine, the only thing I can come up with is that there is something wrong with the database such that UserManager cannot update those particular rows. Unfortunately, a close examination of the User table does not show any specific differences between the 102 affected accounts and any unaffected account - there is no consistency with those 102 accounts that pops out.

Attempts to set both fields to some value through MSSQL, including empty strings, makes no difference.

Suggestions? Frankly, I have no clue what needs showing or what further questions I should be asking at this time.


EDIT

A MASSIVE WHISKY-TANGO-FOXTROT moment here, folks.

Turns out that there IS a commonality between all of the accounts… the USERNAMES all have non-digit, non-integer characters. Like a plus (+) or a dash (-). Problem is, dashes are common in many domain names (to say nothing of the eMail user name itself), and pluses are used extensively in plus-addressing for certain providers like gMail.

I have confirmed that the addition of a plus or a dash in any part of the username causes any sort of a set, delete or reset of a password to fail via UserManager.

I have done this by choosing a user that hasn’t yet made use of their reset link, changed their username so it had a dash, tried (and failed!) to reset the password, then removed the dash and tried again (and succeeded!).

So clearly anything in a username that isn’t a-z, 0-9 and . causes UserManager to fail spectacularly.

How this happens is beyond me. I literally can’t even.

I am looking for direction and guidance on this one, because my mind is nothing more than a multitude of small shrapnel all over my office right now. I need to be able to accept dashes and pluses in usernames without borking the entire password set/delete/reset functionality.

Please also understand that this system leverages the DotNet system -- everything that is happening here is outside the scope of anything I have personally touched. The only possible way that anything is creeping in is that a normal username entry via a web form has its username “sanitized” such that things like dashes and pluses are not actually as such in the UserName database field.

René Kåbis
  • 842
  • 2
  • 9
  • 28
  • Are the answers to this question of any help? https://stackoverflow.com/questions/19460078/configure-microsoft-aspnet-identity-to-allow-email-address-as-username – SpruceMoose Sep 26 '18 at 22:58
  • Unfortunately not. Because of the repository pattern I use, *entering* usernames is not the problem, as I enter it directy using `_unitOfWork`. I can easily sign up someone who has a dash in their username. What I cannot do is change their password after the fact using the standard `_userManager.AddPasswordAsync()` method. I have since come up with a solution (will post that shortly), and have confirmed that logging on with a username with a dash in it is not adversely affected. – René Kåbis Sep 26 '18 at 23:25

1 Answers1

1

I honestly don’t know if my problem is a result of the vagaries of my setup (the Repository Pattern in question) or due to something I actually did (unlikely), however I did come up with a workaround.

Because I am heavy on security and hate putting all eggs into one basket, I will never use third-party logins such as Facebook or Google. This represents a single point of failure that no website should ever burden a user with. As such, I have been able to ignore this extended functionality of Identity 2.0. Does this affect what I do? No clue, but this statement is just for reference in case it does.

What gave me a clue to a workaround was this post.

Now, my _userManager wasn’t derping wholesale, only its ability to write to the DB was, and only when the UserName field of the DB held an email address that contained a plus or a dash. So I am still able to leverage UserManager, I just can’t use its save-to-db features.

For example, my original reset script went like this:

[HttpPost]
[ValidateAntiForgeryToken]
[ValidateSecureHiddenInputs("ResetId")]
public async Task<ActionResult> Reset(ResetViewModel model) {
  if(!ModelState.IsValid) return View("Reset", model);
  try {
    var reset = await CheckReset(model.ResetId);
    if(!(await _userManager.AddPasswordAsync(reset.UserId, model.NewPassword)).Succeeded) throw new Exception(@"We were able to erase the old password, but were unable to set a new password. Please contact [] with all details for a resolution.");
    await ProcessReset(model.ResetId);
    return RedirectToAction("ResetSuccessful", "Home");
  } catch(Exception e) {
    return View("ResetError", new ResetErrorViewModel(e.Message));
  }
}

What failed was the _userManager.AddPasswordAsync(), and it did so silently - no exception, no DB error message. It just failed to insert the new password and blew its cookies all over anything that tried to use it to touch any account with a plus or dash in the UserName.

My revised code is as such:

[HttpPost]
[ValidateAntiForgeryToken]
[ValidateSecureHiddenInputs("ResetId")]
public async Task<ActionResult> Reset(ResetViewModel model) {
  if(!ModelState.IsValid) return View("Reset", model);
  try {
    var r = await CheckReset(model.ResetId);
    var u = await _unitOfWork.UserRepository.FindByIdAsync(r.UserId);
    new UserMap().ResetPassword(u, _userManager.PasswordHasher.HashPassword(model.NewPassword));
    _unitOfWork.UserRepository.Update(u);
    if(await _unitOfWork.SaveChangesAsync() < 1) throw new Exception(@"We were unable to set a new password. Please contact [] with all details for a resolution.");
    await ProcessReset(r.ResetId);
    return RedirectToAction("ResetSuccessful", "Home");
  } catch(Exception e) {
    return View("ResetError", new ResetErrorViewModel(e.Message));
  }
}

Notice the difference? I call the line item using _unitOfWork and only leverage _userManager to provide me with a hashed password -- I leave the saving of that password to the database to _unitOfWork, which works wonderfully!

FYI, the new UserMap().ResetPassword() is a custom mapping class that I prefer to use instead of an automapper due to control issues.

I have extended the leveraging of _userManager to the user registration method as well, and have confirmed that it is now fully functional across both methods.

René Kåbis
  • 842
  • 2
  • 9
  • 28