How can I supply an AntiForgeryToken when posting JSON data using $.ajax?
Asked Answered
C

13

81

I am using the code as below of this post:

First I will fill an array variable with the correct values for the controller action.

Using the code below I think it should be very straightforward by just adding the following line to the JavaScript code:

data["__RequestVerificationToken"] = $('[name=__RequestVerificationToken]').val();

The <%= Html.AntiForgeryToken() %> is at its right place, and the action has a [ValidateAntiForgeryToken]

But my controller action keeps saying: "Invalid forgery token"

What am I doing wrong here?

Code

data["fiscalyear"] = fiscalyear;
data["subgeography"] = $(list).parent().find('input[name=subGeography]').val();
data["territories"] = new Array();

$(items).each(function() {
    data["territories"].push($(this).find('input[name=territory]').val());
});

    if (url != null) {
        $.ajax(
        {
            dataType: 'JSON',
            contentType: 'application/json; charset=utf-8',
            url: url,
            type: 'POST',
            context: document.body,
            data: JSON.stringify(data),
            success: function() { refresh(); }
        });
    }
Csc answered 25/5, 2010 at 17:4 Comment(0)
P
72

You don't need the ValidationHttpRequestWrapper solution since MVC 4. According to this link.

  1. Put the token in the headers.
  2. Create a filter.
  3. Put the attribute on your method.

Here is my solution:

var token = $('input[name="__RequestVerificationToken"]').val();
var headers = {};
headers['__RequestVerificationToken'] = token;
$.ajax({
    type: 'POST',
    url: '/MyTestMethod',
    contentType: 'application/json; charset=utf-8',
    headers: headers,
    data: JSON.stringify({
        Test: 'test'
    }),
    dataType: "json",
    success: function () {},
    error: function (xhr) {}
});


[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        var httpContext = filterContext.HttpContext;
        var cookie = httpContext.Request.Cookies[AntiForgeryConfig.CookieName];
        AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Headers["__RequestVerificationToken"]);
    }
}


[HttpPost]
[AllowAnonymous]
[ValidateJsonAntiForgeryToken]
public async Task<JsonResult> MyTestMethod(string Test)
{
    return Json(true);
}
Postwar answered 24/6, 2014 at 19:19 Comment(7)
Where did you put the public class ValidateJsonAntiForgeryTokenAttribute at?Operatic
I created a folder directly in project root named Filters where I created a class named ValidateJsonAntiForgeryTokenAttribute.cs.Postwar
That still didn't work for me. I created the new .CS file in a folder in my root of my project, have the [ValidateJsonAntiForgeryToken] on my ActionResult, then have the JS exactly as you have it. Chrome Developer Tools "Network > PageName > Headers" shows: __RequestVerificationToken:egrd5Iun...8AH6_t8w2 under Request Headers. What else could be wrong!?Operatic
I have it working now; it was more than likely a caching issue on my end! Thanks! This answer was great!Operatic
Elegant Solution, very nice use of attributes and results in cleaner code.Fool
Simply Awesome!Sticker
This way works for me. thanks for your full example.Grays
P
51

What is wrong is that the controller action that is supposed to handle this request and which is marked with the [ValidateAntiForgeryToken] expects a parameter called __RequestVerificationToken to be POSTed along with the request.

There's no such parameter POSTed as you are using JSON.stringify(data) which converts your form to its JSON representation and so the exception is thrown.

So I can see two possible solutions here:

Number 1: Use x-www-form-urlencoded instead of JSON for sending your request parameters:

data["__RequestVerificationToken"] = $('[name=__RequestVerificationToken]').val();
data["fiscalyear"] = fiscalyear;
// ... other data if necessary

$.ajax({
    url: url,
    type: 'POST',
    context: document.body,
    data: data,
    success: function() { refresh(); }
});

Number 2: Separate the request into two parameters:

data["fiscalyear"] = fiscalyear;
// ... other data if necessary
var token = $('[name=__RequestVerificationToken]').val();

$.ajax({
    url: url,
    type: 'POST',
    context: document.body,
    data: { __RequestVerificationToken: token, jsonRequest: JSON.stringify(data) },
    success: function() { refresh(); }
});

So in all cases you need to POST the __RequestVerificationToken value.

Perfectionist answered 25/5, 2010 at 17:10 Comment(9)
I like this approach and it works... as long as you're not expecting the stringified json object to be hydrated via the MVC 2 Futures/MVC 3 class JsonValueProviderFactory and you handle the hydration yourself manually so you know to ignore the __RequestVerificationToken. If I don't tell the contentType to expect json for $.ajax, then the verification token is handled, but the json object is not hydrated. If I DO tell set json contentType then the Anti-Forgery validation fails. Because of this, I'll be looking at TWith2Sugars solution. But the above indeed does work!Brimstone
if you have the parameters passed into the function containing the ajax call, you can use $.extend to add the token 'unobtrusively'. ` var data = $.extend(parameters, { __RequestVerificationToken: token, jsonRequest: parameters }); `: https://mcmap.net/q/134554/-appending-to-an-objectSain
@Brimstone how did you exactly make your code work? Or how do you define data variable?Easting
@GregOgle That's only in the simpler case where you're not posting JSON, which is already well-covered in this answer with a simple assignment. No need for the CPU ticks of $.extend().Ellett
I have used "Number 2" proposed by @Darin Dimitrov, and in order for it to work I had to remove the following parameters I had for $.ajax: dataType: 'JSON' and also contentType: 'application/json; charset=utf-8', exactly like Darin Dimitrov has it in his post.Preponderate
Also, in the View markup I had it as follows: using (Html.BeginForm(actionName: "", controllerName: "", routeValues: null, method: FormMethod.Post, htmlAttributes: new { @class = "form-validator" })) { @Html.AntiForgeryToken() }Preponderate
I found the following solution that have worked as a charm for me - no need to modify the current models on C# side, and the RequestVerificationToken is being passed through Request Headers: nozzlegear.com/blog/…Preponderate
Eh, comments expire quickly... the passing token through Request Header article, couple updates to make it work (at least for me): remove dataType: 'JSON', just created a function PostJson (without extending jQuery) and added function 'failure', for error : failure - with dataType: 'JSON' it was ALWAYS returning failed Ajax call, even that data submitted by Ajax call WAS successfully submitted into target database.Preponderate
This soulton kind of works for me. I am getting into the method now and the tken is there, but "data" is empty. I used the number 2 solution.Nonfiction
A
10

I was just implementing this actual problem in my current project. I did it for all Ajax POSTs that needed an authenticated user.

First off, I decided to hook my jQuery Ajax calls so I do not to repeat myself too often. This JavaScript snippet ensures all ajax (post) calls will add my request validation token to the request. Note: the name __RequestVerificationToken is used by the .NET framework so I can use the standard Anti-CSRF features as shown below.

$(document).ready(function () {
    securityToken = $('[name=__RequestVerificationToken]').val();
    $('body').bind('ajaxSend', function (elm, xhr, s) {
        if (s.type == 'POST' && typeof securityToken != 'undefined') {
            if (s.data.length > 0) {
                s.data += "&__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
            else {
                s.data = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
        }
    });
});

In your Views where you need the token to be available to the above JavaScript code, just use the common HTML-Helper. You can basically add this code wherever you want. I placed it within a if(Request.IsAuthenticated) statement:

@Html.AntiForgeryToken() // You can provide a string as salt when needed which needs to match the one on the controller

In your controller simply use the standard ASP.NET MVC anti-CSRF mechanism. I did it like this (though I actually used a salt).

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public JsonResult SomeMethod(string param)
{
    // Do something
    return Json(true);
}

With Firebug or a similar tool you can easily see how your POST requests now have a __RequestVerificationToken parameter appended.

Aciculum answered 4/4, 2012 at 15:37 Comment(2)
This works with most types of ajax posts in the MVC from my testing, I've dropped in the Layout and leave it work without any more code.Gisellegish
It looks to me you were missing the point. Your code should only work with a request data payload as application/x-www-form-urlencoded content type. The OP wanted to send his request data payload as application/json. Appending &__Request... to a JSON payload should fail. (He was not asking for a JSON response, which is what do your code sample, but for a JSON request.)Buttons
C
9

You can set $.ajax 's traditional attribute and set it to true, to send json data as url encoded form. Make sure to set type:'POST'. With this method you can even send arrays and you do not have to use JSON.stringyfy or any changes on server side (e.g. creating custom attributes to sniff header )

I have tried this on ASP.NET MVC3 and jquery 1.7 setup and it's working

following is the code snippet.

var data = { items: [1, 2, 3], someflag: true};

data.__RequestVerificationToken = $(':input[name="__RequestVerificationToken"]').val();

$.ajax({
    url: 'Test/FakeAction'
    type: 'POST',
    data: data
    dataType: 'json',
    traditional: true,
    success: function (data, status, jqxhr) {
        // some code after succes
    },
    error: function () {
        // alert the error
    }
});

This will match with MVC action with following signature

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult FakeAction(int[] items, bool someflag)
{
}
Carricarriage answered 11/10, 2013 at 19:3 Comment(1)
This method works when trying to send data that includes arrays of C# value types, however I cannot get it to work when using user defined classes. Do you need to do anything differently to get that to work?Myrna
J
7

You can't validate an content of type contentType: 'application/json; charset=utf-8' because your date will be uploaded not in the Form property of the request, but in the InputStream property, and you will never have this Request.Form["__RequestVerificationToken"].

This will be always empty and validation will fail.

Jadotville answered 12/7, 2011 at 13:37 Comment(0)
A
5

You won't ever have to validate an AntiForgeryToken when you receive posted JSON.

The reason is that AntiForgeryToken has been created to prevent CSRF. Since you can't post AJAX data to another host and HTML forms can't submit JSON as the request body, you don't have to protect your app against posted JSON.

Ascot answered 29/9, 2011 at 20:43 Comment(6)
This is not always correct. It is possible to fake a JSON post using HTML forms. If you look at AjaxRequestExtensions.IsAjaxRequest, it checks within the request body for "X-Requested-With", not just the headers. Thus, either you roll your own validation to ensure that the data is posted with AJAX, or you add AntiForgeryToken.Preponderant
But the body of the request won't be JSON, it will be form-url-encoded.Ascot
who said you can't ajax data to another host? #299245Marianmariana
@AdamTuliper In the case you explicitly want to accept a cross-domain request, you do not want to, at the same time, protect it against cross-domain requests. You want to do one in one situation, and the other in the other situation, not both at the same time.Ascot
Agreed but My statement was in response to 'you can't post Ajax data to another host'. You can post data. If you meant something different Maybe an edit is at hand as it reads like you can't do that.Marianmariana
This would be true if you could require an Action to only be available to AJAX requests, but you can't. As stated. The only thing you would be able to do is lock down the Action to only accept application/json as the body of the request. But I'm not really familiar with how you restrict an Action in MVC to a particular content-type only, you'd have to do a lot of custom work I'm guessing. It's not an out of the box feature as far as I know.Family
K
5

I have resolved it globally with RequestHeader.

$.ajaxPrefilter(function (options, originalOptions, jqXhr) {
    if (options.type.toUpperCase() === "POST") {
        // We need to add the verificationToken to all POSTs
        if (requestVerificationTokenVariable.length > 0)
            jqXhr.setRequestHeader("__RequestVerificationToken", requestVerificationTokenVariable);
    }
});

where the requestVerificationTokenVariable is an variable string that contains the token value. Then all ajax call send the token to the server, but the default ValidateAntiForgeryTokenAttribute get the Request.Form value. I have writed and added this globalFilter that copy token from header to request.form, than i can use the default ValidateAntiForgeryTokenAttribute:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
      filters.Add(new GlobalAntiForgeryTokenAttribute(false));
}


public class GlobalAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    private readonly bool autoValidateAllPost;

    public GlobalAntiForgeryTokenAttribute(bool autoValidateAllPost)
    {
        this.autoValidateAllPost = autoValidateAllPost;
    }

    private const string RequestVerificationTokenKey = "__RequestVerificationToken";
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        var req = filterContext.HttpContext.Request;
        if (req.HttpMethod.ToUpperInvariant() == "POST")
        {
            //gestione per ValidateAntiForgeryToken che gestisce solo il recupero da Request.Form (non disponibile per le chiamate ajax json)
            if (req.Form[RequestVerificationTokenKey] == null && req.IsAjaxRequest())
            {
                var token = req.Headers[RequestVerificationTokenKey];
                if (!string.IsNullOrEmpty(token))
                {
                    req.Form.SetReadOnly(false);
                    req.Form[RequestVerificationTokenKey] = token;
                    req.Form.SetReadOnly(true);
                }
            }

            if (autoValidateAllPost)
                AntiForgery.Validate();
        }
    }
}

public static class NameValueCollectionExtensions
{
    private static readonly PropertyInfo NameObjectCollectionBaseIsReadOnly = typeof(NameObjectCollectionBase).GetProperty("IsReadOnly", BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);

    public static void SetReadOnly(this NameValueCollection source, bool readOnly)
    {
        NameObjectCollectionBaseIsReadOnly.SetValue(source, readOnly);
    }
}

This work for me :)

Kenneth answered 22/3, 2017 at 17:44 Comment(1)
This seems like the best option. An easily found filter (if another developer comes to this project), and allows normal requests that don't need to be "jsonified" to include the normal token in their requests (eg: for projects that already exist and you don't want to change EVERY ajax call in code). And it uses extra headers, and can easily be added via "JQuery.ajaxSetup" to automatically include in headers.Committeeman
C
4

I hold the token in my JSON object and I ended up modifying the ValidateAntiForgeryToken class to check the InputStream of the Request object when the post is json. I've written a blog post about it, hopefully you might find it useful.

Candelabra answered 24/9, 2010 at 14:41 Comment(1)
Link down, information gone. You should have included it in the answer. Now it's too late.Cauliflower
M
2

Check out Dixin's Blog for a great post on doing exactly that.

Also, why not use $.post instead of $.ajax?

Along with the jQuery plugin on that page, you can then do something as simple as:

        data = $.appendAntiForgeryToken(data,null);

        $.post(url, data, function() { refresh(); }, "json");
Maronite answered 9/7, 2010 at 4:41 Comment(0)
E
2

AJAX based model posting with AntiForgerytoken can be made bit easier with Newtonsoft.JSON library
Below approach worked for me:
Keep your AJAX post like this:

$.ajax({
  dataType: 'JSON',
  url: url,
  type: 'POST',
  context: document.body,
  data: {
    '__RequestVerificationToken': token,
    'model_json': JSON.stringify(data)
  };,
  success: function() {
    refresh();
  }
});

Then in your MVC action:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(FormCollection data) {
 var model = JsonConvert.DeserializeObject < Order > (data["model_json"]);
 return Json(1);
}

Hope this helps :)

Ectoenzyme answered 23/10, 2018 at 16:56 Comment(0)
S
1

I had to be a little shady to validate anti-forgery tokens when posting JSON, but it worked.

//If it's not a GET, and the data they're sending is a string (since we already had a separate solution in place for form-encoded data), then add the verification token to the URL, if it's not already there.
$.ajaxSetup({
    beforeSend: function (xhr, options) {
        if (options.type && options.type.toLowerCase() !== 'get' && typeof (options.data) === 'string' && options.url.indexOf("?__RequestVerificationToken=") < 0 && options.url.indexOf("&__RequestVerificationToken=") < 0) {
            if (options.url.indexOf('?') < 0) {
                options.url += '?';
            }
            else {
                options.url += '&';
            }
            options.url += "__RequestVerificationToken=" + encodeURIComponent($('input[name=__RequestVerificationToken]').val());
        }
    }
});

But, as a few people already mentioned, the validation only checks the form - not JSON, and not the query string. So, we overrode the attribute's behavior. Re-implementing all of the validation would have been terrible (and probably not secure), so I just overrode the Form property to, if the token were passed in the QueryString, have the built-in validation THINK it was in the Form.

That's a little tricky because the form is read-only, but doable.

    if (IsAuth(HttpContext.Current) && !IsGet(HttpContext.Current))
    {
        //if the token is in the params but not the form, we sneak in our own HttpContext/HttpRequest
        if (HttpContext.Current.Request.Params != null && HttpContext.Current.Request.Form != null
            && HttpContext.Current.Request.Params["__RequestVerificationToken"] != null && HttpContext.Current.Request.Form["__RequestVerificationToken"] == null)
        {
            AntiForgery.Validate(new ValidationHttpContextWrapper(HttpContext.Current), null);
        }
        else
        {
            AntiForgery.Validate(new HttpContextWrapper(HttpContext.Current), null);
        }
    }

    //don't validate un-authenticated requests; anyone could do it, anyway
    private static bool IsAuth(HttpContext context)
    {
        return context.User != null && context.User.Identity != null && !string.IsNullOrEmpty(context.User.Identity.Name);
    }

    //only validate posts because that's what CSRF is for
    private static bool IsGet(HttpContext context)
    {
        return context.Request.HttpMethod.ToUpper() == "GET";
    }

...

internal class ValidationHttpContextWrapper : HttpContextBase
{
    private HttpContext _context;
    private ValidationHttpRequestWrapper _request;

    public ValidationHttpContextWrapper(HttpContext context)
        : base()
    {
        _context = context;
        _request = new ValidationHttpRequestWrapper(context.Request);
    }

    public override HttpRequestBase Request { get { return _request; } }

    public override IPrincipal User
    {
        get { return _context.User; }
        set { _context.User = value; }
    }
}

internal class ValidationHttpRequestWrapper : HttpRequestBase
{
    private HttpRequest _request;
    private System.Collections.Specialized.NameValueCollection _form;

    public ValidationHttpRequestWrapper(HttpRequest request)
        : base()
    {
        _request = request;
        _form = new System.Collections.Specialized.NameValueCollection(request.Form);
        _form.Add("__RequestVerificationToken", request.Params["__RequestVerificationToken"]);
    }

    public override System.Collections.Specialized.NameValueCollection Form { get { return _form; } }

    public override string ApplicationPath { get { return _request.ApplicationPath; } }
    public override HttpCookieCollection Cookies { get { return _request.Cookies; } }
}

There's some other stuff that's different about our solution (specifically, we're using an HttpModule so we don't have to add the attribute to every single POST) that I left out in favor of brevity. I can add it if necessary.

Samul answered 15/7, 2013 at 23:9 Comment(0)
C
0

Unfortunately for me, the other answers rely on some request formatting handled by jquery, and none of them worked when setting the payload directly. (To be fair, putting it in the header would have worked, but I did not want to go that route.)

To accomplish this in the beforeSend function, the following works. $.params() transforms the object into the standard form / url-encoded format.

I had tried all sorts of variations of stringifying json with the token and none of them worked.

$.ajax({
...other params...,
beforeSend: function(jqXHR, settings){

    var token = ''; //get token

    data = {
        '__RequestVerificationToken' : token,
        'otherData': 'value'
     }; 
    settings.data = $.param(data);
    }
});

```

Carolecarolee answered 19/11, 2015 at 5:16 Comment(1)
Please let me know if there's a bug -- I just typed it up by hand rather than copying and pasting the jumbled mound of attempts that is my real code :\Carolecarolee
P
-1

You should place AntiForgeryToken in a form tag:

@using (Html.BeginForm(actionName:"", controllerName:"",routeValues:null, method: FormMethod.Get, htmlAttributes: new { @class="form-validator" }))
{
    @Html.AntiForgeryToken();
}

Then in javascript modify the following code to be

var DataToSend = [];
DataToSend.push(JSON.stringify(data), $('form.form-validator').serialize());
$.ajax({
  dataType: 'JSON',
  contentType: 'application/json; charset=utf-8',
  url: url,
  type: 'POST',
  context: document.body,
  data: DataToSend,
  success: function() {
    refresh();
  }
});

Then you should be able to validate the request using ActionResult annotations

[ValidateAntiForgeryToken]
        [HttpPost]

I hope this helps.

Pallet answered 20/2, 2014 at 14:15 Comment(1)
if you're using json to post it, it doesn't have to be in a form tag unless you're serializing the form. Your method works but it's unnecessarily complex. You can just append the token to the data.Eer

© 2022 - 2024 — McMap. All rights reserved.