Search code examples
c#asp.net-core.net-coreasp.net-core-3.1unobtrusive-ajax

Using ValidateAntiForgeryToken attribute with Unobtrusive Ajax plugin in ASP.NET Core


I have the following form:

<form asp-action="GetUsersAPICall" asp-controller="UsersObject" asp-antiforgery="true" data-ajax="true" data-ajax-method="get" data-ajax-mode="replace" data-ajax-update="#userSearchResult">
    Enter email or name to search for: <input type="text" id="query" name="query"/>

    <input type="submit" value="Search" />
</form>

<div id="userSearchResult"></div>

(Yes, I do realize that I'm kind of mixing Unobtrusive AJAX syntax with ASP.NET Core tag helpers).

I have the following Action Method in my controller:

    [HttpPost]
    [Authorize(Roles = "PatchUser")]
    //[ValidateAntiForgeryToken]
    public async Task<IActionResult> PatchUserAPICall(UserPatchViewModel vm)
    {
        if (vm == null)
        {
            return BadRequest();
        }
        else if (!ModelState.IsValid)
        {
            return View(vm);
        }
        else
        {
            bool result = await vm.User.Update();

            if (result)
            {
                return RedirectToAction("Confirmation");
            }
            else
            {
                return StatusCode((int)HttpStatusCode.InternalServerError);
            }
        }
    }

This works just fine, unless I uncomment the ValidateAntiForgeryToken attribute.

I've seen examples of doing validation for jQuery AJAX calls, but many of those rely on sending it in the headers (e.g. in this Q&A). Supposedly, this will make the ValidateAntiForgeryToken just work as expected (with a few configuration changes in code). Hopefully I'm not missing something obvious, but I've searched quite a bit and can't find how to actually add a header in Unobtrusive AJAX, so I haven't even be able to try something like that to see if it would work.

I know that you can do User.IsInRole("RoleName") as an alternative to the Authorize attribute. Is there a way to send the Anti-Forgery Token as a parameter and do it that way? Or is there a way to edit the headers and do it that way? Or is there a better way to do this that I haven't thought of yet?

In general, is there some way to edit the Ajax call before it's sent?


Solution

  • First of all, your data-ajax-method="get" should be data-ajax-method="post". This is because your use of asp-antiforgery="true" will add a hidden __RequestVerificationToken to your form. Normally this hidden field is submitted along with the rest of your form data. However, the __RequestVerificationToken cannot be submitted in the query string (i.e., GET), it must be submitted either in the header or in the body (i.e., POST). If you make this change, the whole thing is likely to "just work."

    However, it is also possible to get the Antiforgery token into the header of the ajax request. jquery unobtrustive ajax doesn't do this out of the box, but it's not hard to add in. In jquery.unobtrusive-ajax.js, there is a function, asyncRequest(element, options){..} at the end of that function, is the actual jquery ajax call:

    $.ajax(options);
    

    Just before this call, on line 143 in the linked code, insert the following:

    if (method === "POST") {
        var token = $("input[name='__RequestVerificationToken'][type='hidden']").val();
        if (token) {
            options.headers = { RequestVerificationToken: token };
        }
    }
    

    This can also be useful if you're doing an ajax POST using an <a> tag for a delete or something:

    @Html.AntiforgeryToken() //add the hidden antiforgery token, assuming you don't have a form tag.
    <a href="#" data-ajax="true" data-ajax-method="post" 
      data-ajax-url="@Url.Action("Delete", new {Model.Id})" data-ajax-confirm="Are you sure?" 
      data-ajax-success="alert('success')">Delete This</a>