Sending an anti-forgery token with Razor Pages AJAX requests
Hey, before you start reading! I am in the market, looking for new freelance employment opportunities. If you need assistance on any of your ASP.NET Core projects, I am available for hire for freelance work.
ASP.NET Razor Pages uses anti-forgery tokens to protect websites against Cross-site request forgery (CSRF) attacks. When posting information to a Razor Page handler, you need to take special care to send this anti-forgery token otherwise the request fails. This blog post looks at a couple of techniques you can use to ensure the anti-forgery token is sent with your AJAX POST requests.
For background information on how anti-forgery tokens work in Razor Pages, I suggest you read Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core on the ASP.NET Documentation website.
The problem
Before I discuss the possible solutions, I would like to demonstrate the problem you may be facing when making AJAX POST requests to Razor Pages. Let us assume you have the following HTML form fields defined.
<div class="form-group">
<label for="FirstName">FirstName</label>
<input id="FirstName" class="form-control">
</div>
<div class="form-group">
<label for="LastName">FirstName</label>
<input id="LastName" class="form-control">
</div>
<button id="AddUser" class="btn btn-primary">Add User</button>
When the user clicks on the button, the values from the fields will be posted to the Razor Pages backed via an AJAX request. The JavaScript that handles this are as follows:
const addUserButton = document.getElementById('AddUser');
addUserButton.addEventListener('click', function() {
const firstNameField = document.getElementById('FirstName');
const lastNameField = document.getElementById('LastName');
const postUrl = '@LinkGenerator.GetUriByPage(HttpContext, handler: "IndividualFields")';
const formData = new FormData();
formData.append('FirstName', firstNameField.value);
formData.append('LastName', lastNameField.value);
fetch(postUrl, {
method: 'post',
body: formData
}).then(function(response) {
console.log(response);
});
});
The JavaScript code registers an event listener that listens for the click
event on the button element. When the user clicks on the button, an AJAX request is made to the Razor Pages page handler using the Fetch API.
If we run this application and click on the button, you notice in the browser developer tools that the request fails with an HTTP Status Code 400 (Bad Request).
This can be a tricky problem to track down since the response body of the HTTP request does not provide any more information. At first glance, the application logs also do not appear to provide any more information as there are no warnings present. The problem only reveals itself when you change the log settings to log informational messages.
You can see the following error in the log:
Antiforgery token validation failed. The required antiforgery request token was not provided in either form field "__RequestVerificationToken" or header value "RequestVerificationToken"
As you can see from the log message, the request failed because no anti-forgery token was provided with the request. There are a couple of ways to solve this problem, both of which are reasonably simple to implement.
Solution 1: Send the anti-forgery token as a request header
The first solution to the problem is to send the anti-forgery token as a header in the AJAX request. To do that we need to inject an instance of the IAntiforgery
interface into your Razor Page.
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery AntiForgery;
When making the AJAX request, pass the anti-forgery token in the RequestVerificationToken
header by making a call to GetAndStoreTokens()
to generate and store an AntiforgeryTokenSet
and passing the value of the RequestToken
property.
fetch(postUrl, {
method: 'post',
body: formData,
headers: {
'RequestVerificationToken': '@AntiForgery.GetAndStoreTokens(HttpContext).RequestToken'
}
}).then(function(response) {
console.log(response);
});
Now, when making the AJAX request, you can see that the anti-forgery token is passed as a header and the request succeeds.
Solution 2: Generate a form and post the form data
The second solution (which is my preferred approach) is to generate a standard HTML form element with the Razor tag helpers and then posting the form data via an AJAX request.
Let’s create a form in the same way you would if you were doing a regular postback to post the form data.
<form method="post" asp-page-handler="RawFormData" id="postDataForm">
<div class="form-group">
<label asp-for="Data.FirstName"></label>
<input asp-for="Data.FirstName" class="form-control">
</div>
<div class="form-group">
<label asp-for="Data.LastName"></label>
<input asp-for="Data.LastName" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
In this scenario, we register a submit
event handler on the form and post the actual form data, rather than retrieving the values of the individual fields.
const postDataForm = document.getElementById('postDataForm');
postDataForm.addEventListener('submit', function(e) {
e.preventDefault();
const postUrl = this.action;
const formData = new FormData(this);
fetch(postUrl, {
method: 'post',
body: formData
}).then(function(response) {
console.log(response);
});
})
Notice that in this scenario, we do not post a header with the anti-forgery token. The reason we do not have to do this is because, when the application executes, a hidden field is generated containing the anti-forgery token.
When the AJAX request is made, the value of this field is sent along with the values of all the other form fields.
Conclusion
In this blog post, we looked at a common issue you may run into when posting information via AJAX to a Razor Pages page handler where requests fail because an anti-forgery token is not present. We also looked at two different ways you can send the anti-forgery token, either via a request header or in the request body.
Source code for this blog post is available at https://github.com/jerriepelser-blog/RazorPagesAjaxAntiForgery