1
votes

I have a viewmodel class that called "LoginIndexViewModel" for use on login razor page, containing login, signin & forget password forms. It contains several properties, each of which is a viewmodel separately. Here is the "LoginIndexViewModel" viewmodel:

public class LoginIndexViewModel
{
    public LoginViewModel Login { get; set; }

    public SignUpViewModel SignUp { get; set; }

    public ForgetPasswordViewModel ForgetPassword { get; set; }
}

On "SignUpViewModel" there is a property that has remote validation and I want to check anti-forgery-token before action method call. Here is the body of the "SignUpViewModel":

public class SignUpViewModel
{
    .
    .
    .

    [Display(Name = "Email *")]
    [DataType(DataType.EmailAddress)]
    [Required(ErrorMessage = "The Email Is Required.")]
    [EmailAddress(ErrorMessage = "Invalid Email Address.")]
    [RegularExpression("^[a-zA-Z0-9_\\.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "Invalid Email Address.")]
    [Remote("CheckUsername", "Account", ErrorMessage = "The Email Address Have Already Registered.", HttpMethod = "POST", **AdditionalFields = "__RequestVerificationToken")]
    public string Username { get; set; }

    .
    .
    .
}

I used [ValidateAntiForgeryToken] attribute above the action method "CheckUsername" and @Html.AntiForgeryToken() on the razor view such as the following:

[HttpPost]
[AjaxOnly]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public virtual async Task<JsonResult> CheckUsername([Bind(Prefix = "SignUp.UserName")]string userName)
{
    return Json(await _userManager.CheckUsername(username), JsonRequestBehavior.AllowGet);
}

razor view:

 @using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
 {
   @Html.AntiForgeryToken()
   .
   .
   .
   <div class="form-group">
       @Html.TextBoxFor(m => m.SignUp.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off", @Value = "" })
       <span class="helper">@Html.ValidationMessageFor(model => model.SignUp.UserName)</span>
   </div>
   .
   .
   .
 }

The problem is that remote validation call throws the exception: 'The required anti-forgery form field "__RequestVerificationToken" is not present.'

According to the Mozilla debugger tool, I found that CheckUsername calls with 'SignUp.__RequestVerificationToken' parameter instead of '__RequestVerificationToken' therefore it raise an exception.

Anyone knows that what happened and why __RequestVerificationToken is not providing ?

1

1 Answers

1
votes

The addition of the prefix to properties defined in AdditionalFields is by design, and is added by jquery.validate.unobtrusive.js. The relevant code is that adds the prefix is in the adapters.add("remote", ["url", "type", "additionalfields"], function (options) { method.

There are numerous options to solve your problem

Create your own custom (say) [ValidateAntiForgeryTokenWithPrefix] attribute based on the existing source code, but modify to strip any prefix from the request (not recommended)

Make your own ajax call, rather than using the [Remote] attribute - i.e. handle the .blur() event of the textbox to call the server method, passing both the value and the token, and update the placeholder generated by @Html.ValidationMessageFor() in the success callback, and handle the .keyup() event to clear any message. This does have a performance advantage because after initial validation, the remote rule makes an ajax call on every keyup() event, so it can result in a lot of server and database calls.

However the easiest solution would be to create 3 partials based on LoginViewModel, SignUpViewModel and ForgetPasswordViewModel and call then from the main view using @Html.Partial(), for example

_SignUpViewModel.cshtml

@model SignUpViewModel
....
@using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
{
    @Html.AntiForgeryToken()
    ....
    <div class="form-group">
        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off" })
        <span class="helper">
            @Html.ValidationMessageFor(model => model.UserName)
        </span>
    </div>
    ....
}

and in the main view

@model LoginIndexViewModel
....
@Html.Partial("_SignUpViewModel", Model.SignUp)
.... // ditto for Login and ForgetPassword properties

You can then omit the [Bind(Prefix = "SignUp.UserName")] from the CheckUsername() method.

Alternatively you make the main view based on say LoginViewModel, and then use @Html.Action() to call [ChildActionOnly] methods that return partial views of the other 2 forms.

Having said that, a user will only ever use the SignUp form once, and may never use the ForgetPassword form, so including all that extra html is just degrading performance, and it would be better to have links to redirect them to separate pages for SignUp and ForgetPassword, or if you want them in the same page, then load partials for them on demand using ajax.

As a side note, you do not need the JsonRequestBehavior.AllowGet argument in your return Json(...) (its a [HttpPost] method), and you should never set the value attribute when using HtmlHelper methods.