Search code examples
asp.net-coreasp.net-identityasp.net-membershipidentityserver4

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.


Solution

  • 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!