Justin
Justin

Reputation: 18236

Maintain passwords while switching from ASP.NET Membership to ASP.NET Core Identity

My company is planning to upgrade our applications from .NET Framework to .NET Core, and as part of that to upgrade from ASP.NET Membership to ASP.NET Core Identity server. I found a useful article on this here.

However, there is a sub-note with massive implications:

After completion of this script, the ASP.NET Core Identity app created earlier is populated with Membership users. Users need to change their passwords before logging in.

We can't ask 600,000 users to change their password as part of this migration. However, the membership passwords are one-way hashed, so we can't retrieve them and then migrate them. So I'm wondering how we would go about maintaining our existing users' passwords with the new Identity Server approach.

Upvotes: 5

Views: 2794

Answers (2)

mackie
mackie

Reputation: 5264

We've recently migrated from various legacy systems and since they all used various forms of hashing passwords, rather than trying to port that logic over we customised the password authentication code to allow it to do a call out to an API exposed by each legacy system. Each user migrated from such a system had the API URL stored against it.

When a migrated user signs in for the first time we make a call out to said service (which itself was secured using a bearer token and a restricted integration scope) to do the password authentication that first time. If we get a success response then we hash the password in our own format and that is used forever more.

The downside of this is that you do have to keep the old system up (with this new API bolted on) pretty much forever. Since it's all .Net you may fare better by keeping it all in-proc and copying the migrated user hashed passwords to your new DB, assuming you can get an implementation of the old hashing scheme running inside .Net Core.

Upvotes: 0

Linda Lawton - DaImTo
Linda Lawton - DaImTo

Reputation: 117301

I did this quite recently.

We had a legacy .net membership system and needed to import about 10k users over to asp.net identity. I first created an extra column in the asp .net identity core user table when i copied all of the users from the system i brought their legacy password with them.

Then when the user logged in the first time. I first checked if the legacy password existed if it did then i validated them against that and update the password on asp. net identity core and deleted the legacy password. This way all of the users ported their passwords to the new system without even realizing it.

I am going to try and explain how i did it but the code is a bit crazy.

I actually added two columns to the applicationuser table

public string LegacyPasswordHash { get; set; }
public string LegacyPasswordSalt { get; set; }

ApplicationSignInManager -> CheckPasswordSignInAsync method does a check if the user is a legacy user

ApplicationSignInManager

public override async Task<SignInResult> CheckPasswordSignInAsync(ApplicationUser user, string password, bool lockoutOnFailure)
        {
        ........

            if (user.IsLegacy)
            {
                Logger.LogDebug(LoggingEvents.ApplicationSignInManagerCheckPasswordSignInAsync, "[user.Id: {user.Id}] is legacy.", user.Id);
                var results = await new LoginCommand(_logger, _userManager, user, password, lockoutOnFailure).Execute();
                if (results.Succeeded)
                {
                    await ResetLockout(user);
                    return SignInResult.Success;
                }
            }
            else if (await UserManager.CheckPasswordAsync(user, password))
            {
                await ResetLockout(user);
                return SignInResult.Success;
            }

            ........
        }

Login Command

 public class LoginCommand
    {
        private readonly ILogger _logger;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly ApplicationUser _user;
        private readonly string _password;
        private readonly bool _shouldLockout;

        public LoginCommand(ILogger logger, UserManager<ApplicationUser> userManager, ApplicationUser user, string password, bool shouldLockout)
        {
            _logger = logger;
            _userManager = userManager;
            _user = user;
            _password = password;
            _shouldLockout = shouldLockout;
        }

        public async Task<SignInResult> Execute()
        {
            _logger.LogInformation($"Found User: {_user.UserName}");
            if (_user.IsLegacy)
                return await new LegacyUserCommand(_logger, _userManager, _user, _password, _shouldLockout).Execute();
            if (await _userManager.CheckPasswordAsync(_user, _password))
                return await new CheckTwoFactorCommand(_logger, _userManager, _user).Execute();
            if (_shouldLockout)
            {
                return await new CheckLockoutCommand(_logger, _userManager, _user).Execute();
            }
            _logger.LogDebug($"Login failed for user {_user.Email} invalid password");
            return SignInResult.Failed;
        }
    }

LegacyUserCommand

  public class LegacyUserCommand
    {
        private readonly ILogger _logger;
        private readonly UserManager<ApplicationUser> _userManager;

        private readonly ApplicationUser _user;
        private readonly string _password;
        private bool _shouldLockout;

        public LegacyUserCommand(ILogger logger, UserManager<ApplicationUser> userManager, ApplicationUser user, string password, bool shouldLockout)
        {
            _logger = logger;
            _userManager = userManager;
            _user = user;
            _password = password;
            _shouldLockout = shouldLockout;
        }

        public async Task<SignInResult> Execute()
        {
            try
            {
                if (_password.EncodePassword(_user.LegacyPasswordSalt) == _user.LegacyPasswordHash)
                {
                    _logger.LogInformation(LoggingEvents.LegacyUserCommand, "Legacy User {_user.Id} migrating password.", _user.Id);
                    await _userManager.AddPasswordAsync(_user, _password);
                    _user.SecurityStamp = Guid.NewGuid().ToString();
                    _user.LegacyPasswordHash = null;
                    _user.LegacyPasswordSalt = null;
                    await _userManager.UpdateAsync(_user);
                    return await new CheckTwoFactorCommand(_logger, _userManager, _user).Execute();
                }
                if (_shouldLockout)
                {
                    _user.SecurityStamp = Guid.NewGuid().ToString();
                    await _userManager.UpdateAsync(_user);
                    _logger.LogInformation(LoggingEvents.LegacyUserCommand, "Login failed for Legacy user {_user.Id} invalid password. (LockoutEnabled)", _user.Id);
                    await _userManager.AccessFailedAsync(_user);
                    if (await _userManager.IsLockedOutAsync(_user))
                        return SignInResult.LockedOut;
                }

                _logger.LogInformation(LoggingEvents.LegacyUserCommand, "Login failed for Legacy user {_user.Id} invalid password", _user.Id);
                return SignInResult.Failed;
            }
            catch (Exception e)
            {
                _logger.LogError(LoggingEvents.LegacyUserCommand, "LegacyUserCommand Failed for [_user.Id: {_user.Id}]  [Error Message: {e.Message}]", _user.Id, e.Message);
                _logger.LogTrace(LoggingEvents.LegacyUserCommand, "LegacyUserCommand Failed for [_user.Id: {_user.Id}] [Error: {e}]", _user.Id, e);
                return SignInResult.Failed;
            }
        }
    }

TOP TIP: [SecurityStamp] an not be NULL!

Upvotes: 4

Related Questions