400 Bad Request when POST-ing to Razor Page - asp.net-mvc

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>

Related

How to use a custom "__RequestVerificationToken"?

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 }
});

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.

How to call local MVC 4 function inside ajax call

In my SPA website i need to send antiforrgery token over to server by ajax. I use this article method:
http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-%28csrf%29-attacks
So in my layout.cshtml i have local function :
<script>
#functions{
public string TokenHeaderValue()
{
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
return cookieToken + ":" + formToken;
}
}
</script>
and i have separate js file named register.js where i call this function inside ajax:
$.ajax({
url: 'User/JsonRegister',
type: "POST",
data: d,
headers: {
'RequestVerificationToken': '#TokenHeaderValue()'
},
success: function (result) {
},
error: function (result) {
}
});
the problem is that i never #TokenHeaderValue() is never called and i m keep getting this error:
The required anti-forgery form field "__RequestVerificationToken" is not present.
How do i solve this problem?
The problem is using server side helper function in the separate js file. If you need to have ajax call in the separate js file, set #TokenHeaderValue() value to a hidden field and read the value of hidden field in the js file.
i add token to my data this way: which solves problem
AddAntiForgeryToken = function (data) {
data.__RequestVerificationToken = $('#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]').val();
return data;
};
Pass data in JSON format
$.ajax({
type:"POST",
datatype:"JSON",
data:{d:d}, // Remaining code is same
});
And use [HttpPost] before method signature in controller because it a Post request.

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.

Session Cookies expiration handling in ASP.NET MVC 3 while using WIF and jquery ajax requests

I my project I'm using WIF (but this is not really important for the context of this question. You can use alternative framework which handles your authentication. Question is about dealing with authentication failures while performing ajax requests). Nevertheless, in my case I've written custom server logic which inherits from ClaimsAuthenticationManager, and handles authentication:
public override IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
{
if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated)
{
// add some custom claims
}
return incomingPrincipal;
}
Now, after I delete all Session Cookies, end then enter any page again, I'm redirected to the login page served by WIF, and I'm requested to log again. Everything works as expected.
But if I make an ajax request instead, I've got an error, which is intercepted by this:
$(document).ready(function () {
$.ajaxSetup({
error: function (XMLHttpRequest, textStatus, errorThrown) {
// do something
}
});
});
Unfortunately XMLHttpRequest object does not return any meaningful message, based on which I could handle this kind of error in any other way as others. In this particular case I just want application to redirect to the login page - as the normal request does.
While the ajax call is executing, the method Authenticate from ClaimsAuthenticationManager is invoked. Identity.IsAuthenticated returns false, method ends and all is done. Even the OnAuthorization method from BaseController is not invoked, so I cannot pass any status to the ajax result object.
protected override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest() && !User.Identity.IsAuthenticated)
{
//do something, for example pass custom result to filterContext
}
base.OnAuthorization(filterContext);
}
How to resolve the puzzle ?
I've found some resources about this (see bottom of the answer), and mixed up with following solution:
while performing ajax request, I specified that I want json back:
$.ajax({
url: action,
type: 'POST',
dataType: 'json',
data: jsonString,
contentType: 'application/json; charset=utf-8',
success:
function (result, textStatus, xhr) {
}
});
Because my framework handles authentication, while token expires, it puts http status 302 to the response. Because I don't want my browser to handle 302 response transparently, I catch it in Global.asax, and changed status to 200 OK. Aditionally, I've added header, which instructs me to process such response in special way:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == 302
&& (new HttpContextWrapper(Context)).Request.IsAjaxRequest())
{
Context.Response.StatusCode = 200;
Context.Response.AddHeader("REQUIRES_AUTH", "1");
}
}
Response content is not properly serialized to json, which results in parsing error. Error event is invoked, inside which the redirection is performed:
$(document).ready(function () {
$.ajaxSetup({
error: function (XMLHttpRequest, textStatus, errorThrown) {
if (XMLHttpRequest.getResponseHeader('REQUIRES_AUTH') === '1') {
// redirect to logon page
window.location = XMLHttpRequest.getResponseHeader('location');
}
// do sth else
}
});
});
See How to manage a redirect request after a jQuery Ajax call and here How do you deal with AJAX requests when user session expires, or where the request terminates in a 302 for more explanation.
UPDATE:
Meantime, I figured out new solution, in my opinion much better, because can be applied to all ajax requests out of the box (if they do not redefine beforeSend event obviously):
$.ajaxSetup({
beforeSend: checkPulse,
error: function (XMLHttpRequest, textStatus, errorThrown) {
document.open();
document.write(XMLHttpRequest.responseText);
document.close();
}
});
function checkPulse(XMLHttpRequest) {
var location = window.location.href;
$.ajax({
url: "/Controller/CheckPulse",
type: 'GET',
async: false,
beforeSend: null,
success:
function (result, textStatus, xhr) {
if (xhr.getResponseHeader('REQUIRES_AUTH') === '1') {
XMLHttpRequest.abort(); // terminate further ajax execution
window.location = location;
}
}
});
}
The controller method can be anything simplest:
[Authorize]
public virtual void CheckPulse() {}
The Application_EndRequest() stays the same as before.

Resources