2

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();
}
Community
  • 1
  • 1
Hypi
  • 127
  • 1
  • 8
  • How are you generaing the token? The token doesn't contain anything to do with the password so it's very unclear how the error you see is relevant to the issue you claim. – DavidG Apr 14 '17 at 12:06
  • I've added the code snippets – Hypi Apr 14 '17 at 12:13
  • On which line do you see the exception happen? – DavidG Apr 14 '17 at 12:21
  • I see a few issues here. First `GeneratePasswordResetTokenAsync` is supposed to take the user ID, not the user name. Second, you are URL encoding the code when you don't need to, `Url.Action` will handle that for you. – DavidG Apr 14 '17 at 12:32
  • the error is returned here – Hypi Apr 14 '17 at 12:37
  • `var result = await _userManager.ResetPasswordAsync(user.UserName, HttpUtility.UrlDecode(model.Code), model.Password);` – Hypi Apr 14 '17 at 12:37
  • 1
    Don't do the encoding and decoding MVC will handle that for you. – DavidG Apr 14 '17 at 12:38
  • I now figured out, that it is not the previous password that has an asterisk in it that is the problem... it is the new password that is the problem. When putting in a password with a different special char it works – Hypi Apr 14 '17 at 12:38
  • yay @DavidG that solved the problem I just took out the encoding and decoding completely and it worked like a charm -.- If you want to create an answer of it I will mark it as the correct answer – Hypi Apr 14 '17 at 12:59

1 Answers1

9

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);
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • Would inserting the `userId = user.UserName` in the callback url be a violation of cwe-204 "Observable Response Discrepancy"? – display-name Oct 15 '21 at 16:27