Migrated from Membership to Identity, logging in old users now gives Base-64 error

Vote:
 

Hello,

I'm migrating our newly updated CMS10/Commerce website from using the old .net Membership references to Episerver's aspnetidentity.

I followed the directions on this page, and all seemed to work well:

http://world.episerver.com/documentation/Items/Developers-Guide/Episerver-CMS/9/Security/episerver-aspnetidentity/

Next, I successfully ran a script that imported all our users from the old membership tables to the new aspnet identity tables.

Whenever I try to log on using an old login I get the following error:

The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. 

Exception Details: System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters. 

Source Error: 

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace: 


[FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters. ]
   System.Convert.FromBase64_ComputeResultLength(Char* inputPtr, Int32 inputLength) +12271472
   System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength) +71
   System.Convert.FromBase64String(String s) +42
   Microsoft.AspNet.Identity.Crypto.VerifyHashedPassword(String hashedPassword, String password) +53
   Microsoft.AspNet.Identity.PasswordHasher.VerifyHashedPassword(String hashedPassword, String providedPassword) +11
   Microsoft.AspNet.Identity.d__3e.MoveNext() +292
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +92
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +58
   Microsoft.AspNet.Identity.d__17.MoveNext() +257
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +92
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +58
   Microsoft.AspNet.Identity.d__12.MoveNext() +448
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +92
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +58
   Microsoft.AspNet.Identity.AsyncHelper.RunSync(Func`1 func) +281
   Microsoft.AspNet.Identity.UserManagerExtensions.Find(UserManager`2 manager, String userName, String password) +167
   EPiServer.Cms.UI.AspNetIdentity.ApplicationSignInManager`1.SignIn(String userName, String password, String returnUrl) +117
   EPiServer.Cms.UI.AspNetIdentity.ApplicationUISignInManager`1.SignIn(String providerName, String userName, String password) +44
   EPiServer.UI.WebControls.Login.OnAuthenticate(AuthenticateEventArgs e) +77
   System.Web.UI.WebControls.Login.AttemptLogin() +119
   System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e) +75
   System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +37
   System.Web.UI.WebControls.Button.OnCommand(CommandEventArgs e) +114
   System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument) +260
   System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument) +12
   System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +15
   System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) +35
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1639


After some searching, I tried implementing the following fix: http://sveinaandahl.blogspot.se/2016/03/how-to-validate-old-passwords-when.html

I added a UserManager Class:

using CMS.Models.ViewModels.Register;
using EPiServer.Cms.UI.AspNetIdentity;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace CMS.Models
{
    public class CustomUserManager : ApplicationUserManager
    {
        public CustomUserManager() : base(new UserStore(new ApplicationDbContext()))
        {
            this.PasswordHasher = new SQLPasswordHasher();
        }
    }

    public class SQLPasswordHasher : PasswordHasher
    {
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }

        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create("HMACSHA512");
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length;)
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
        }
    }
}

And then I modified my StartUp.cs:

using CMS.Models;
using EPiServer.Cms.UI.AspNetIdentity;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using System;


[assembly: OwinStartup("customStartup", typeof(CMS.Startup))]

namespace CMS
{
    public class Startup
    {

        public void Configuration(IAppBuilder app)
        {
            // Add CMS integration for ASP.NET Identity
            app.AddCmsAspNetIdentity(new ApplicationOptions() { ConnectionStringName = "EcfSqlConnection" });
            // Use cookie authentication
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/util/login.aspx"),
                Provider = new CookieAuthenticationProvider
                {
                    OnValidateIdentity =
                         SecurityStampValidator.OnValidateIdentity(
                             validateInterval: TimeSpan.FromMinutes(30),
                             regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user))
                }
            });
        }


    }

}

But it appears to have no effect, I still receive the same error every time.  Has anyone dealt with this or fixed this that can help? 

Thanks!

John

#183207
Oct 06, 2017 22:36
Vote:
 

If you didn't delete the old user tables, then you can use the old membership provider to try a login, if that is a success, then you can update the password on with the identity provider.

The way I did this task was it is:

  1. Change all passwords to something that is valid, so you don't get the error you get now
  2. Try to login with new method (SignInManager)
  3. if that fails, try to login with old membership provider
    • if success, update password on the identity provider with the password the user used
    • if error, show that you couldn't login

Something like

[HttpPost]
public async Task<ActionResult> InternalLogin(LoginViewModel viewModel)
{
    var result = await SignInManager.PasswordSignInAsync(viewModel.Email, viewModel.Password, viewModel.RememberMe, shouldLockout: true);
    switch (result)
    {
        case SignInStatus.Success:
            break
        case SignInStatus.LockedOut:
            return PartialView("~/Views/Features/Login/_lockout.cshtml", viewModel)
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", "Login", new { ReturnUrl = viewModel.ReturnUrl, RememberMe = viewModel.RememberMe })
        default:
            return await UpdatePasswordFromOldSystem(viewModel);
    
    return Json(new { ReturnUrl = returnUrl });
}
public async Task<ActionResult> UpdatePasswordFromOldSystem(LoginViewModel viewModel)
{
    // the password was correct, but it wasn't in new system, update it
    if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
    {
        UserManager.PasswordValidator = new PasswordValidator() // turn off validating requirements
        {
            RequiredLength = 0,
            RequireNonLetterOrDigit = false,
            RequireDigit = false,
            RequireLowercase = false,
            RequireUppercase = false
        };
        SiteUser user = await UserManager.FindByNameAsync(viewModel.Email); // get the user
        string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); // get reset token
        IdentityResult reset = await UserManager.ResetPasswordAsync(user.Id, code, viewModel.Password); // reset the password with the password the user used and token we got

        if (reset.Succeeded) // if it succeeded, then try login again
        {
            return await InternalLogin(viewModel);
        }
    }
    
    // wrong username/password, show error
    ModelState.AddModelError("Password", _localizationService.GetString(() => Translations.Login.WrongPasswordOrEmail));
    viewModel.Password = null;
    return PartialView("~/Views/Features/Login/_login.cshtml", viewModel);
}

Remember to change password on the old provider too when a user change password :) (I know this is a litle hacky way, but it works for me at least :))

Oh, and this is the migration script i ran (after deployment so the tables get made)

INSERT INTO AspNetUsers(
                Id,
                Email,
                EmailConfirmed,
                PasswordHash,
                SecurityStamp,
                PhoneNumberConfirmed,
                TwoFactorEnabled,
                LockoutEnabled,
                AccessFailedCount,
                UserName,
                NewsLetter,
                IsApproved,
                IsLockedOut,
                Comment,
                CreationDate,
                LastLoginDate
)
SELECT 
aspnet_Users.UserId,
aspnet_Membership.Email,
1,
('AFPMQQQEExOuGGdX/PYqosGS4johxSt7NycCbbxRG/VtYAywHKYeNR5genTrTdOB7g=='),
'0b039082-4d6d-4fab-b36b-01492e58e9c4',
0,
0,
0,
0,
aspnet_Users.UserName,
0,
1,
0,
aspnet_Membership.Comment,
ISNULL(aspnet_Membership.CreateDate, GETDATE()),
aspnet_Membership.LastLoginDate

FROM aspnet_Users
LEFT OUTER JOIN aspnet_Membership ON aspnet_Membership.ApplicationId = aspnet_Users.ApplicationId 
AND aspnet_Users.UserId = aspnet_Membership.UserId;

INSERT INTO AspNetRoles(Id,Name)
SELECT RoleId,RoleName
FROM aspnet_Roles;

INSERT INTO AspNetUserRoles(UserId,RoleId)
SELECT UserId,RoleId
FROM aspnet_UsersInRoles;
#183210
Edited, Oct 07, 2017 0:15
Vote:
 

try this version for SQL Password hasher https://stackoverflow.com/questions/46074408/migration-of-asp-net-membership-to-mvc-net-identity-password-issue

#183221
Oct 09, 2017 12:29
Vote:
 

Thanks for the suggestions guys, they're all good but after finally getting a lot of sleep this weekend I resolved this issue, I had made a stupid mistake in my LoginController in which I was calling the ApplicationUserManager instead of my CustomUserManager, therefore the users were never getting to my passwordhasher.

#183259
Oct 09, 2017 21:56
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.