1

I have an old MVC website I'm looking at. I don't understand the following code in my Account controller. (I believe this was code generated by ASP.NET.)

Controller

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

//
// 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.Email);
    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();
}

View

@model OnBoard101.Models.ResetPasswordViewModel
@{
    ViewBag.Title = "Reset password";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Reset your password</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Code)
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-success" value="Reset" />
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

These methods are called when the user uses the Forgot Password feature and then clicks the link that is sent to them in an email.

As best I can tell, the POST handler correctly detects an invalid code in the link (it produces an Invalid Token error when the code is not value). But I don't understand how. The GET handler seems to simply discard the code argument.

I don't get how this code works. How does model.Code ever get populated?

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • That seems indeed like a weird way to do the trick. If you're only checking that the code's not null on the GET stage, you could call it with a random string as the query parameter and it'd simply return the view. Is the `model.Code` there when the POST happens? – Mariano Luis Villa Jun 05 '21 at 21:20
  • @MarianoLuisVilla: I found that the post method does in fact give an error if the code is not valid. But I don't see how it has access to that code. – Jonathan Wood Jun 06 '21 at 14:28
  • Interesting. Maybe there's some custom base controller or attribute doing the pass-through? Looking at the `ResetPassword` view should give you a clear idea of where the code's being placed on. – Mariano Luis Villa Jun 06 '21 at 15:16
  • I'm pretty sure the query string from get is preserved in post. Can you check the generated action url for the embedded form? – Mat J Jun 08 '21 at 14:43
  • I had this exact issue when I was beginning in old mvc framework and [asked about it](https://stackoverflow.com/q/6690150/219933). The urlbuilder uses existing route values also if its same route, and model is populated from query string and body if anywhere there is a matching argument. – Mat J Jun 08 '21 at 15:15
  • @JonathanWood model binding also inspect the query string parameters for matching properties. It retrieves data from various sources such as route data, form fields, and query strings. – Nkosi Jun 08 '21 at 16:21
  • @Nkosi: At what stage? Are you talking about when it calls the POST handler? – Jonathan Wood Jun 08 '21 at 17:52
  • @JonathanWood Check more detailed answer provided below – Nkosi Jun 08 '21 at 18:02

1 Answers1

1

The ResetPassword feature does use code from the link.

Model binding retrieves data from various sources such as route data, form fields, and query strings.

It inspects the query string parameters for matching properties on the model used by the view

While it may appear like the code is being discarded by the Controller GET action, the code is still a part of the request and used by the view.

And since the view explicitly binds to a model

@model OnBoard101.Models.ResetPasswordViewModel

which has a matching public Code property (case-insensitive)

public string Code { get; set; }

it will bind it to the model in the view during the GET and then use it (the model) to populate a hidden form field (as shown in your View markup)

@Html.HiddenFor(model => model.Code)

So now when the form is submitted, the POST action will bind that field to the model posted back to the action and perform the validation

The same could have been achieved with

// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code) {
    if (code == null) return View("Error");
    var model = new ResetPasswordViewModel {
        Code = code
    };
    return View(model);
}

But since the built-in model binding functionality would initialize a model if one was not provided, the above code really does not add anything additional to what the framework does out-of-the-box.

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 1
    That's neat! Altough explicitly passing the code would make it easier to understand at a maintenance stage, IMO. – Mariano Luis Villa Jun 08 '21 at 18:25
  • Thanks. Although the view is bound to a model, I always had the impression it was an empty model unless I provided a model instance to the view. I didn't know the view would collect data from the query string. – Jonathan Wood Jun 08 '21 at 19:16