Sending an anti-forgery token with Razor Pages AJAX requests

December 04, 2019
ASP.NET Core 3.1

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).

Bad Request when POSTing without the anti-forgery token

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.

Log message with the underlying cause of the request failure

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.

Successful AJAX request with anti-forgery token passed as header

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.

Hidden field generate with 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.

The anti-forgery token sent with the form data

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

PS: If you need assistance on any of your ASP.NET Core projects, I am available for hire for freelance work.