How to implement "reset password" in MVC 5 API?

Stesvis 1,041 Reputation points
2023-06-23T23:45:03.78+00:00

Long ago the web app was done in ASP.NET pages and I had the reset password functionality implemented with Views and Controllers:

    [Authorize]
    public class AccountController : BaseController
    {
		//...

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

                string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
                var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
                await UserManager.SendEmailAsync(user.Id, "Reset Password", $"Please reset your password by clicking <a href =\"{callbackUrl}\">here</a>");

                return RedirectToAction("ForgotPasswordConfirmation", "Account");
            }

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

        // POST: /Account/ResetPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var user = await UserManager.FindByNameAsync(model.Username);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }
            var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }
            AddErrors(result);
            return View();
        }
		
		//...
	}

Where UserManager is:

private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
    get
    {
        return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    }
    private set
    {
        _userManager = value;
    }
}

Now the web app is a React app, so I had to translate these actions in API endpoints.

The problem is that I can't get them to work:

    [Authorize]
    [RoutePrefix("api/v3.1/Account")]
    public class Account_v3_1Controller : Base_v3_1ApiController
    {
		//...
        private ApplicationUserManager _userManager;
        public ApplicationUserManager UserManager
        {
            get
            {
                if (_userManager == null)
                {
                    _userManager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
                }
                return _userManager;
            }
            private set
            {
                _userManager = value;
            }
        }
		
		
		//...
        // POST api/Account/SetPassword
        [AllowAnonymous]
        [HttpPost]
        [Route("SendResetPasswordLink")]
        public async Task<IHttpActionResult> SendResetPasswordLink(ForgotPasswordViewModel model)
        {
            try
            {
                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

                if (ModelState.IsValid)
                {
                    var user = await UserManager.FindByNameAsync(model.Username);
                    if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
                    {
                        // Don't reveal that the user does not exist or is not confirmed
                        return BadRequest("Username not found.");
                    }

                    string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
                    //var callbackUrl = Url.Link("Default", new
                    //{
                    //    Controller = "account",
                    //    Action = "reset-password",
                    //    userId = user.Id,
                    //    code,
                    //});

                    var link = $"{System.Configuration.ConfigurationManager.AppSettings["AppUrl"]}/account/reset-password?userId={user.Id}&code={HttpUtility.UrlEncode(code)}";
                    await UserManager.SendEmailAsync(user.Id, "Reset Password", $"Please reset your password by clicking <a href =\"{link}\">here</a>.");

                    return Ok("Please check your email and click on the reset password link.");
                }

                return BadRequest();
            }
            catch (Exception ex)
            {
                AppLogger.ExceptionLogger.Error(ex);
                await HandleException(ex);
                return InternalServerError(ex);
            }
        }

        // POST api/Account/ResetPassword
        [AllowAnonymous]
        [HttpPost]
        [Route("ResetPassword")]
        public async Task<IHttpActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var user = await UserManager.FindByNameAsync(model.Username);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return BadRequest("User not found.");
            }

            var result = await UserManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(model.Code), model.Password);

            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }

            return Ok();
        }

		//...
		
	}

The code is generated, emailed correctly embedded in the link, etc. etc.

However, when I consume the API POST api/Account/ResetPassword I always get "Invalid token" in the line with await UserManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(model.Code), model.Password);

I tried with and without the UrlEncode/UrlDecode.

Obviously in the API world something is different and there is a mismatch, maybe an issue with UserManager?

Can anyone help? I am sure every app out there has this feature implemented, thanks!

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,798 questions
ASP.NET API
ASP.NET API
ASP.NET: A set of technologies in the .NET Framework for building web applications and XML web services.API: A software intermediary that allows two applications to interact with each other.
331 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. QiYou-MSFT 4,321 Reputation points Microsoft Vendor
    2023-06-26T07:59:04.7933333+00:00

    Hi @Stesvis

    The parameters of the UserManager.ResetPasswordAsync method are (User, Token, NewPassWord). According to your mistake, there is a problem with the Token.

    The problem should be HttpUtility.UrlDecode(model. Code) modified the code.

    So you can change the code to UserManager.ResetPasswordAsync(user. Id, model. Code, model. Password).

    I tried with and without the .UrlEncode/UrlDecode

    I don't quite understand the meaning of this sentence, have you already tried my method?

    If you have further questions, please leave me a Comment.

    Best Regards

    Qi You


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.