Cross-site request forgery (CSRF) with ASP.NET Core and AJAX

ASP.NET Core comes with built-in support for cross-site request forgery (CSRF) checks in both old school form posts and AJAX requests. I believe the examples in the official documentation is hard to understand and requires you to change every request made through jQuery or similar frameworks to make server requests. For this week's post, I want to share how we've implemented CSRF checks with ASP.NET Core and AJAX.

Cross-site request forgery (CSRF) with ASP.NET Core and AJAX

I've written about CSRF before: The ultimate guide to secure cookies with web.config in .NET. To recap, CSRF is the practice of cheating the user into requesting a website where he/she is already logged in, by including hidden forms, image element, and more. To avoid this scenario you need to look into the SameSite cookie additions as explained in the linked post. You should also generate a CSRF token on your client and send it to the server for validation on all POST, PUT, and DELETE requests.

ASP.NET Core automatically injects a hidden CSRF token in all form elements without an action attribute and you should insert one manually in the rest of your forms. In a classic web application, Postback is a common pattern where a form POST to the server and the server redirects the browser to a new GET request. When implementing a single page application with Vanilla.js, Angular, React, or similar, posts are made asynchronously with AJAX.

To implement CSRF in an ASP.NET Core application, you want to decorate all actions with an attribute. In the long run, it's easier to include it as a public filter:

services
    .AddControllersWithViews(options =>
    {
        options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
    });

In this example, I'm calling the AddControllersWithViews method but the approach is similar when calling the AddMvc method or one of the other alternatives. By adding the AutoValidateAntiforgeryTokenAttribute class, all POST, PUT, and DELETE requests are automatically checked for the presence of a valid token. You'd normally not validate GET requests but in case you do, use the ValidateAntiForgeryTokenAttribute class instead.

As mentioned earlier, MVC will inject CSRF tokens in all action-less forms. For AJAX requests initiated through JavaScript, you will need to provide your own CSRF token. In this example, I'm using jQuery but similar solutions can be used for other frameworks.

To test the error returned by the CSRF check, let's include a bit of JavaScript in the site.js file or where ever you write JavaScript in your project:

$(document).ready(function () {
    $.post("/home/post");
});

The code makes an empty post to an action that we can include in HomeController:

[HttpPost]
public IActionResult Post()
{
    return Ok();
}

Hit F5 and see what happens:

POST failed

ASP.NET Core failed to find a valid token and returns a status code of 400. To include the token in every request, start by including the following Razor code just before the </body> tag:

@Html.AntiForgeryToken()

This will generate a hidden <input> element with a CSRF token looking like this:

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8D3coQ0eo-9FihbOkfTdm5K4iaWDlp1yE3ciyFjA7FfCVlfEyyNLsu8Py50neE2yWPB0r8pdiV1FjRj-I7NgUbX2aqZdz0enZvIY5utbLGKZjsrfzqvPEf-lsswKTC4gmcgElt1pg67VryXWrE8gV6o" />

To send the generated token with every AJAX request, include the following code:

var e = $('input[name="__RequestVerificationToken"]').val();
$(document)
    .ajaxSend(function (t, a, i) {
        a && i && ("POST" === i.type || "PUT" === i.type || "DELETE" === i.type) && a.setRequestHeader("RequestVerificationToken", e);
    });

For all POST, PUT, and DELETE requests made through jQuery, we include the RequestVerificationToken header with the value from the previously generated <input> element. Notice how the <input> element has __ as a prefix while the header doesn't. That's because the AutoValidateAntiforgeryTokenAttribute class expects the name without the underscores. You can overwrite the name of the header if you'd like by including the following configuration in Startup.cs:

services.AddAntiforgery(options =>
{
    options.HeaderName = "__RequestVerificationToken";
});

Here's the full JavaScript code for this post:

$(document).ready(function () {
    var e = $('input[name="__RequestVerificationToken"]').val();
    $(document)
        .ajaxSend(function (t, a, i) {
            a && i && ("POST" === i.type || "PUT" === i.type || "DELETE" === i.type) && a.setRequestHeader("RequestVerificationToken", e);
        });

    $.post("/home/post");
});

Every AJAX request you write from now on will automatically submit the token to the server and it will get validated. If you have one or a few controllers that shouldn't validate the token, this can be done using the IgnoreAntiforgeryTokenAttribute class:

[IgnoreAntiforgeryToken]
public class SpecialController : Controller
{
}

Examples of controllers and actions that you don't want to validate the token could be to support integrations with other systems where the other system needs to call your API. A public API would typically not have CSRF checks either since another security mechanism is normally in play (like API keys or OAuth).

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website