1

I'm developing a MVC5 ASP.Net application.
I'm using Identity 2.2.0 for authentication.
Everything is OK, but I can't reset my password, because of Invalid Token error.
The following are ResetPassword related actions in Account controller.

[AllowAnonymous]
public ActionResult ForgotPassword()
{
    return View();
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (!ModelState.IsValid) return View(model);
    ApplicationUser userModel = await UserManager.FindByNameAsync(model.Email);

    if (userModel == null)
        ModelState.AddModelError("", "The user doesn't exist");

    if (userModel != null && !await UserManager.IsEmailConfirmedAsync(userModel.Id))
        ModelState.AddModelError("", "The user email isn't confirmed");

    if (!ModelState.IsValid) return View();

    var user = _userService.GetUserByEmail(model.Email);

    // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
    // Send an email with this link

    string websiteTitle = StaticAssests.WebsiteTitle;
    string emailContext = _settingService.GetValue(SettingNames.ResetPasswordMailFormat);
    string code = await UserManager.GeneratePasswordResetTokenAsync(userModel.Id);
    string callbackUrl = Url.Action("ResetPassword", "Account", new { userId = userModel.Id, code }, Request.Url.Scheme);
    emailContext = emailContext.Replace("{userfullname}", user.FullName);
    emailContext = emailContext.Replace("{websitetitle}", websiteTitle);
    emailContext = emailContext.Replace("{websitedomain}", StaticVariables.WebsiteDomain);
    emailContext = emailContext.Replace("{username}", userModel.UserName);
    emailContext = emailContext.Replace("{resetpasswordurl}", callbackUrl);
    emailContext = emailContext.Replace("{date}", new PersianDateTime(DateTime.Now).ToLongDateTimeString());
    await UserManager.SendEmailAsync(userModel.Id, string.Format("Reset password {0}", websiteTitle), emailContext);
    return RedirectToAction("ForgotPasswordConfirmation", "Account");
}

[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
    return View();
}
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
    return code == null ? View("Error") : View();
}

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (!ModelState.IsValid) return View(model);
    ApplicationUser userModel = await UserManager.FindByNameAsync(model.Email);
    if (userModel == null)
    {
        ModelState.AddModelError("", "The user doesn't exist");
        return View();
    }
// Invalid Token error
        IdentityResult result = await UserManager.ResetPasswordAsync(userModel.Id, model.Code, model.Password);
        if (result.Succeeded)
        {
            return RedirectToAction("ResetPasswordConfirmation", "Account");
        }
        AddErrors(result);
        return View();
    }

I've checked the followings:
1. Resetting email send successfully.
2. GeneratePasswordResetTokenAsync run without any problem. and the generated code with it is the same with code argument in ResetPassword action.

IdentityConfig:

public class ApplicationUserManager : UserManager<ApplicationUser, int>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser, int> userStore) : base(userStore)
        {

        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            ApplicationUserManager applicationUserManager = new ApplicationUserManager(new ApplicationUserStore());
            //ApplicationUserManager applicationUserManager = new ApplicationUserManager(context.Get<ApplicationUser>());
            //new ApplicationUserManager(new UserStore<UserModel>(context.Get<ApplicationDbContext>()));
            // Configure validation logic for usernames
            //applicationUserManager.UserValidator = new UserValidator<UserIdentityModel, int>(applicationUserManager)
            //{
            //  AllowOnlyAlphanumericUserNames = false,
            //  RequireUniqueEmail = true,
            //};
            applicationUserManager.PasswordValidator = new MyMinimumLengthValidator(6);
            applicationUserManager.UserValidator = new MyUserModelValidator();
            applicationUserManager.PasswordHasher = new MyPasswordHasher();

            // Configure user lockout defaults
            applicationUserManager.UserLockoutEnabledByDefault = true;
            applicationUserManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            applicationUserManager.MaxFailedAccessAttemptsBeforeLockout = 5;

            // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
            // You can write your own provider and plug in here.
            applicationUserManager.RegisterTwoFactorProvider("PhoneCode",
                new PhoneNumberTokenProvider<ApplicationUser, int>
                {
                    MessageFormat = "Your security code is: {0}"
                });
            applicationUserManager.RegisterTwoFactorProvider("EmailCode",
                new EmailTokenProvider<ApplicationUser, int>
                {
                    Subject = "Security code",
                    BodyFormat = "your security code is {0}"
                });

            applicationUserManager.EmailService = new EmailService();
            applicationUserManager.SmsService = new SmsService();
            IDataProtectionProvider dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                applicationUserManager.UserTokenProvider =
                    new DataProtectorTokenProvider<ApplicationUser, int>(dataProtectionProvider.Create("ASP.NET Identity"));
            }

            return applicationUserManager;
        }
    }

What's wrong?

Update:
I've changed User Id from string to int.

1 Answer 1

4

I know this is an old question, but I've run into this on two projects and both times the issue was the exact same thing. Perhaps this answer might help others. The token being passed includes special characters that cannot be used as is within a URL string without causing problems.

When passing the token to the front end UI, be sure to URLEncode the token, something like this:

var callbackUrl =
                new Uri(string.Format("{0}/resetpassword?userId={1}&code={2}",
                    ConfigurationManager.AppSettings["websiteUrl"], user.Id,
                    WebUtility.UrlEncode(token)));

When the token is passed back into the back end, Decode the token before passing it to the password ResetPassword function:

var result = await this.AppUserManager.ResetPasswordAsync(appUser.Id, WebUtility.UrlDecode(resetPasswordBindingModel.ConfirmCode),
             resetPasswordBindingModel.NewPassword);

Both of the projects where I had this issue were running HTML/MVC/JavaScript on the front end and the validations were being done over ASP.NET WebAPI 2.x.

One other note: UrlDecode, for some reason, can't properly decode the plus symbol and you get a space in the token instead of the '+'. The trick I used is to just use a string replace to convert any spaces to + signs. It's not ideal, but I've not had a problem with it.

1
  • Thanks, I'll test it. Commented Nov 18, 2015 at 4:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.