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!