Search code examples
c#asp.net.netpasswordsasp.net-identity

ASP.Net Identity “Invalid token” on password reset with * in password


We get the invalid token error messages when a user tries to reset his password on the reset password screen after entering the new password. Normally this works just fine for everyone even with some special character like #. We have now a case where someone puts in * in his new password on the reset pw screen, gets this error message just because of this special character.

I've tried hours of research now to find a solution to why this happens but with no luck. I've found this solution here which has an issue with special characters in the username but we don't have that issue. There is only an issue with that special character in the password. As we are already in production we can't just disallow that character in passwords.

Someone got a clue?

Generating the token controller method:

[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByNameAsync(model.Email.ToLower());
        if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.UserName)))
        {
            // Don't reveal that the user does not exist or is not confirmed
            return View("ForgotPasswordConfirmation");
        }

        // 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
        var code = await _userManager.GeneratePasswordResetTokenAsync(user.UserName);
        code = HttpUtility.UrlEncode(code);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.UserName, code = code }, protocol: Request.Url.Scheme);

        await _emailService.CreateResetPasswordEmailAsync(user, callbackUrl);
        return RedirectToAction("ForgotPasswordConfirmation", "Account");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Reset password controller method:

[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await _userManager.FindByNameAsync(model.Email.ToLower());
    if (user == null)
    {
        // Don't reveal that the user does not exist
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }

    var result = await _userManager.ResetPasswordAsync(user.UserName, HttpUtility.UrlDecode(model.Code), model.Password);
    if (result.Succeeded)
    {
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }

    AddErrors(result);
    return View();
}

Solution

  • The problem is that you are double encoding the reset token. Here:

    var code = await _userManager.GeneratePasswordResetTokenAsync(user.UserName);
    code = HttpUtility.UrlEncode(code);  //<--problem is this line
    var callbackUrl = Url.Action("ResetPassword", "Account", 
        new { userId = user.UserName, code = code }, protocol: Request.Url.Scheme);
    

    you encode the token and then Url.Action will do it again. So the solution is to not encode manually and let MVC handle it for you - just remove the second line here.

    Also, on the other end, there's now no need to decode again, so your code there will be:

    var result = await _userManager.ResetPasswordAsync(user.UserName, 
        model.Code, model.Password);