How to use a custom "__RequestVerificationToken"? - asp.net-mvc

I've made a partial view like this (location: MyController/_Form.cshtml):
<form asp-antiforgery="true">
<input type="button" value="submit" />
</form>
Some actions in Controller:
[HttpPost, ValidateAntiForgeryToken]
public IActionResult Test()
{
return Ok(new { succeeded = true });
}
[HttpPost]
public IActionResult GetTemplate()
{
string template = _viewRender<string>("MyController/_Form", null);
return Ok({ template = template });
}
The _viewRender is a service to convert from partial view to a string.
I've tested with these steps:
Using jquery to make a request from client to server to get the template and append to some div.
let onSuccess = function (data) {
$(data.template).appendTo('.myDiv');
};
$.ajax({
url: '/MyController/GetTemplate',
method: 'POST'
}).done(onSuccess).fail(onError);
And the event to detect submiting form looks like:
$(document).on('click', 'input[type=text]', function () {
let _this = $(this);
let token = _this.parent().find('[name=__RequestVerificationToken]').val();
let onSuccess = function (data) {
console.log(data); // should be: Object:{succeeded:true}
};
$.ajax({
url: '/MyController/Test',
method: 'POST',
data: { __RequestVerificationToken: token },
processData: false,
contentType: false
}).done(onSuccess).fail(onError);
});
When I made the request, I always got error code 404 - not found on Console tab.
I'm sure the path is correct. So, I've tried to remove ValidateAntiForgeryToken attribute from Test action and tried again. It's working fine (request status code 200).
So, I guess the problem (which gave 404 error) came from the token. I've used developer tool to check again and I'm sure that I have a token. But I don't know how to check the token is valid or not.
The token was generated from server. I just made a request to get it and appended to body. Then, re-sent it to server. But server didn't accept it.
Why?

This is how it's done in ASP.NET Core...
In Startup.cs you'll need to setup the anti-forgery header name.
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
You need to do this because by default anti-forgery will only consider form data and we want it to work with ajax too.
In your .cshtml file you'll need to add #Html.AntiForgeryToken() which will render a hidden input with the validation token.
Finally in your ajax code you need to setup the request header before sending.
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
}
So in your case the ajax code will look like this.
$.ajax({
url: '/MyController/Test',
method: 'POST',
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
processData: false,
contentType: false
}).done(onSuccess).fail(onError);

Two things.
First, I use a custom filter instead of ValidateAntiForgeryToken. I don't remember why. Probably ValidateAntiForgeryToken doesn't work with AJAX requests.
Here's the code for the custom filter I use.
[AttributeUsage(AttributeTargets.Class)]
public sealed class ValidateAntiForgeryTokenOnAllPostsAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException(nameof(filterContext));
}
var request = filterContext.HttpContext.Request;
// Only validate POSTs
if (request.HttpMethod == WebRequestMethods.Http.Post)
{
// Ajax POSTs and normal form posts have to be treated differently when it comes
// to validating the AntiForgeryToken
if (request.IsAjaxRequest())
{
var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];
var cookieValue = antiForgeryCookie?.Value;
AntiForgery.Validate(cookieValue, request.Headers["__RequestVerificationToken"]);
}
else
{
new ValidateAntiForgeryTokenAttribute().OnAuthorization(filterContext);
}
}
}
}
Second, the token goes in the request header not the data part. I add it to the header using ajaxSetup in the layout file. That way I don't have to worry about remembering to add it to every AJAX request.
$.ajaxSetup({
cache: false,
headers: { "__RequestVerificationToken": token }
});

Related

400 Bad Request when POST-ing to Razor Page

My page has...
#page "{candidateId:int}"
... and
#Html.AntiForgeryToken()
Model has...
public void OnGet(int candidateId)
{
}
public void OnPost(int candidateId)
{
}
GET works fine. Here is my AJAX request..
$.ajax({
type: "POST",
url: "/Skills/" + candidateId,
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
data: {
name: 'hi mum'
},
success: function (response) {
},
failure: function (response) {
alert(response);
}
});
Browser receives useless error message... 400 Bad Request.
What am I missing?
You are getting a 400 (Bad Request) response because the framework expects the RequestVerificationToken as part of the posted request.The framework uses this to prevent possible CSRF attacks. If your request does not have this information, the framework will return the 400 bad request. Your current code is not sending it.
Change the code to this
headers:
{
"RequestVerificationToken": $('input:hidden[name="__RequestVerificationToken"]').val()
},
This will add a new item with key RequestVerificationToken to the request header and the framework should not throw a 400 response when the call is made. (assuming your view code generated the hidden input for the __RequestVerificationToken hidden input)
You can make the code more robust by injecting the IAntiforgery implementation to the view/page and using the GetAndStoreTokens method.
#inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
#functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Model.HttpContext).RequestToken;
}
}
and call this GetAntiXsrfRequestToken function to get the value in your javascript
headers:
{
"RequestVerificationToken": '#GetAntiXsrfRequestToken()'
},
You also probably want to use the PageModel's CandidateId property to create the url. Something like this
url: "/Skills/#Model.CandidateId",
Also, you do need to call #Html.AntiForgeryToken() method explicitly to generate the token input. Having a form with post method with no action attribute value will generate the hidden input for you.
<form method="post">
<!-- your inputs-->
</form>

Asp.net MVC - How to check session expire for Ajax request

We are using Ajax call across the application- trying to find out a global solution to redirect to login page if session is already expired while trying to execute any Ajax request. I have coded following solution taking help from this post - Handling session timeout in ajax calls
NOT SURE WHY IN MY CARE EVENT "HandleUnauthorizedRequest" DOES NOT GET FIRED.
Custom Attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CheckSessionExpireAttribute :AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
var url = new UrlHelper(filterContext.RequestContext);
var loginUrl = url.Content("/Default.aspx");
filterContext.HttpContext.Session.RemoveAll();
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.HttpContext.Response.Redirect(loginUrl, false);
filterContext.Result = new EmptyResult();
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
Using Above custom attribute as follow in controller action:
[NoCache]
[CheckSessionExpire]
public ActionResult GetSomething()
{
}
AJAX Call(JS part):
function GetSomething()
{
$.ajax({
cache: false,
type: "GET",
async: true,
url: "/Customer/GetSomething",
success: function (data) {
},
error: function (xhr, ajaxOptions, thrownError) {
}
}
Web Config Authentication settings:
<authentication mode="Forms">
<forms loginUrl="default.aspx" protection="All" timeout="3000" slidingExpiration="true" />
</authentication>
I am try to check it by deleting browser cooking before making ajax call but event "CheckSessionExpireAttribute " does not get fired- any idea please.
Thanks,
#Paul
If I got the question right (and even if I didn't, thanks anyway, helped me solve my own situation), what you wanted to avoid was having your login page to load inside an element which was supposed to display a different View via Ajax. That or get an exception/error status code during a Ajax form post.
So, in short, the annotation class will need to override 2 methods, not just HandleUnauthorizedRequest, and it will redirect to a JsonResult Action that will generate the parameters for your Ajax function to know what to do.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class SessionTimeoutAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
base.OnAuthorization(filterContext);
if (!user.Identity.IsAuthenticated) {
HandleUnauthorizedRequest(filterContext);
}
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new RedirectToRouteResult(new
RouteValueDictionary(new { controller = "AccountController", action = "Timeout" }));
}
}
}
Then set this annotation in your authentication Action, so every time it gets called, it will know where the request came from, and what kind of return it should give.
[AllowAnonymous]
[SessionTimeout]
public ActionResult Login() { }
Then your redirected Json Action:
[AllowAnonymous]
public JsonResult Timeout()
{
// For you to display an error message when the login page is loaded, in case you want it
TempData["hasError"] = true;
TempData["errorMessage"] = "Your session expired, please log-in again.";
return Json(new
{
#timeout = true,
url = Url.Content("~/AccountController/Login")
}, JsonRequestBehavior.AllowGet);
}
Then in your client function (I took the privilege of writing it as $.get() instead of $.ajax():
$(document).ready(function () {
$("[data-ajax-render-html]").each(function () {
var partial = $(this).attr("data-ajax-render-html");
var obj = $(this);
$.get(partial, function (data) {
if (data.timeout) {
window.location.href = data.url;
} else {
obj.replaceWith(data);
}
}).fail(function () {
obj.replaceWith("Error: It wasn't possible to load the element");
});
});
});
This function replaces the html tag with this data-ajax-render-html attribute, which contains the View address you want to load, but you can set it to be loaded inside the tag by changing replaceWith for the html() property.
I think that is only a client-side problem.
In web server you can just use the classic Authorize attribute over actions or controllers.
That will validate that the request is authenticated (if there's a valid authentication cookie or authorization header) and sets HTTP 401 if not authenticated.
Note: a session will automatically be recreated if you don't send authorization info in the request, but the request will not be authorized
Solution
Then the javascript client you must handle the redirect (browsers do it automatically but with ajax you need to do it manually)
$.ajax({
type: "GET",
url: "/Customer/GetSomething",
statusCode: {
401: function() {
// do redirect to your login page
window.location.href = '/default.aspx'
}
}
});
I checked and tested the code, looks like clearly.. the problem is that the ajax call is wrong..
I fix Ajax code, try this..
function GetSomething() {
$.ajax({
cache: false,
type: "GET",
async: true,
url: "/Customer/GetSomething",
success: function (data) {
},
error: function (xhr, ajaxOptions, thrownError) {
}
});
}
On HttpContext.Request.IsAjaxRequest()
Please see this related article on why an Ajax request might not be recognized as such.
XMLHttpRequest() not recognized as a IsAjaxRequest?
It looks like there is a dependency on a certain header value (X-Requested-With) being in the request in order for that function to return true.
You might want to capture and review your traffic and headers to the server to see if indeed the browser is properly sending this value.
But, are you even sure it's hitting that line of code? You might also want to debug with a break point and see what values are set.
On Session vs Authentication
Authorization and Session timeout are not always exactly the same. One could actually grant authorization for a period longer than the session, and if the session is missing, rebuild it, as long as they are already authorized. If you find there is something on the session that you'd be loosing that can't be rebuilt, then perhaps you should move it somewhere else, or additionally persist it somewhere else.
Form Authentication cookies default to timeout after 30 minutes. Session timeout default is 20 minutes.
Session timeout in ASP.NET
HandleUnauthorizedRequest not overriding
Sorry to say that: The solution you need is impossible. The reason is:
To redirect user to login page, we have 2 methods: redirect at server, redirect at client
In your case, you're using Ajax so we have only 1 method: redirect at client (reason is, basically, Ajax means send/retrieve data to/from server. So it's impossible to redirect at server)
Next, to redirect at client. Ajax need to information from server which say that "redirect user to login page" while global check session method must be return Redirect("url here").
clearly, global check session method can not return 2 type (return Redirect(), return Json,Xml,Object,or string)
After all, I suggest that:
Solution 1: Don't use ajax
Solution 2: You can use ajax, but check session timeout method at server which is not globally. Mean that you must multiple implement (number of ajax call = number of implement)

Ajax Request issue with ASP.NET MVC 4 Area

Today i discovered something weird, i have regular asp.net mvc 4 project with no such ajax (just post, get). so today i need ajax request, i did an ajax action with jquery in controller and it didn't work out. Here is my code
Areas/Admin/Controllers/BannersController
public JsonResult SaveOrder(string model)
{
bool result = false;
if (!string.IsNullOrEmpty(model))
{
var list = Newtonsoft.Json.JsonConvert.DeserializeObject<List<int>>(model);
result = repository.SaveOrder(list);
}
return Json(result, JsonRequestBehavior.AllowGet);
}
View side (Its in area too)
$(document).ready(function () {
$("#saveOrder").click(function () {
var data = JSON.stringify($("#list_banners").nestable('serialize'));
$.ajax({
url: '#Url.Action("SaveOrder", "Banners", new { area = "Admin" })',
data: { model: data },
success: function (result) {
if (result) {
toastr.success('Kaydedildi.');
}
else {
toastr.error('kaydedilemedi.');
}
},
error: function (e) {
console.log(e);
}
});
});
});
i've already tried everything i know, which is $.post, $.get, ajax options, trying request from out of area etc.. just request can't reach action
and here is the errors ,
http://prntscr.com/297nye
error object
http://prntscr.com/297o3x
Try by specifying the data format (json) you wand to post to server like and Also change the way you pass data object in JSON like this :
var data = $("#list_banners").nestable('serialize');
$.ajax({
url: '#Url.Action("SaveOrder", "Banners", new { area = "Admin" })',
data: JSON.stringify({ model: data }),
dataType: 'json',
contentType: "application/json",
...
I had same issue, but after spending too much time, got solution for that. If your request is going to your specified controller, then check your response. There must be some problem in your response. In my case, response was not properly converted to JSON, then i tried with passing some values of response object from controller using select function, and got what i needed.

Errors when passing data via jquery ajax

I have a pretty simple ajax request that I'm sending over to server in order to get some data and fill up my edit modal. But for some reason it keeps returning with error and I can't figure out why. I've debugged the server side, parameter comes in correctly and all data is properly found and returned, still an error though.
Here's my code so someone might see what am I missing here.
Request:
function EditNorm(id) {
$.ajax({
type: "POST",
url: "#Url.Action("GetNormViewModel")",
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({id : id}),
cache: false,
success: function(data) {
FillFormForEditing(data.nvm);
},
error: function() {
alert("Error On EditNorm function");
}
});
}
Server side:
public JsonResult GetNormViewModel(int id)
{
var nvm = new NormViewModel {Norm = db.Norms.Find(id), Materials = db.Materials.ToList()};
return Json(new {nvm = nvm}, JsonRequestBehavior.AllowGet);
}
Firstly: You are using a POST method on your javascript while your controller accepts a Get, add this to your action:
[HttpPost]
public JsonResult GetNormViewModel(int id)
{
return Json(new { ... });
}
Secondly: What is db is it LinqToSQL / Entity Framework context? If so, make sure no call to your data context is performed after the data is returned. (i.e. changed your action and simply return return Json(new { nvm = "test" }); and console.log/alert to make sure you've got the result back. This will tells you that its your model that failed when it's returned due to some late binding.

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

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(); }
});
}
You don't need the ValidationHttpRequestWrapper solution since MVC 4. According to this link.
Put the token in the headers.
Create a filter.
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);
}
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.
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.
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)
{
}
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.
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 :)
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.
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.
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");
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 :)
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.
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);
}
});
```
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.

Resources