Understanding Anti-Forgery in ASP.NET Core
In today’s web applications, security is a top priority. One of the common attacks that web developers need to guard against is Cross-Site Request Forgery (CSRF). ASP.NET Core provides built-in support to protect against such attacks using Anti-Forgery tokens.
Let’s explore what CSRF is, look at the default settings in ASP.NET Core, and how to implement Anti-Forgery in MVC, Razor Pages, and Minimal APIs. We will also cover handling Anti-Forgery tokens when using XHR or fetch requests originating from JavaScript and considerations for load-balanced scenarios.
What is Cross-Site Request Forgery (CSRF)?
Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks a user’s browser into performing an unwanted action on a different site where the user is authenticated. This can lead to unauthorized actions such as changing account details, making purchases, or even deleting data.
Example of a CSRF Attack
Here’s an example of how a CSRF attack works:
- A user logs into their banking website (example.com) and receives an authentication cookie.
- Without logging out, the user visits a malicious website.
- The malicious site submits a hidden form to example.com/transfer, using the user’s authentication cookie.
- The bank processes the transfer without the user’s knowledge or consent.
To ensure that forms can only be submitted from pages that the legitimate website generated, you can use an anti-forgery token.
Note: Cross-Origin Resource Sharing (CORS) is a security feature that restricts web pages from making HTTP requests to a domain different from the one that served the web page. While CORS prevents a malicious site from reading responses from your application, it doesn’t stop CSRF attacks. Even with strict CORS policies, a malicious site can still trick a user into submitting a form to your application. Both CORS and CSRF protections are essential: CORS controls access to resources based on origin, while CSRF ensures that requests are made with the user’s intent.
ASP.NET Core and CSRF - Anti-Forgery
ASP.NET Core has built-in support for Anti-Forgery tokens to help prevent CSRF attacks. By default, ASP.NET Core includes Anti-Forgery tokens in forms and validates them on the server side. The framework automatically generates and validates these tokens for Razor Pages and MVC applications.
When ASP.NET Core generates an Anti-Forgery token, part of the token is stored in a cookie (.AspNetCore.Antiforgery
by default), and the other part is included in the form or request header. The server then compares these two pieces to verify the request’s authenticity.
The generated cookie containing the anti-forgery token uses the SameSiteMode.Strict
and is HttpOnly
, which means it cannot be accessed by JavaScript code running in the browser, making it impossible for malicious code to steal the cookie value.
Note that the ASP.NET Core Anti-Forgery token is also bound to the current user. The validation will fail when the user data embedded in the Anti-Forgery token doesn’t match the authenticated user.
An example of why that matters is a common Duende IdentityServer support issue when a user has two open browser tabs to the same client app. Each tab challenges the user to sign in with IdentityServer. Each tab successfully renders the login with a unique anti-forgery token for the anonymous user. Remember, the user in both tabs is unknown because they haven’t logged in yet. The user then successfully signs in on the first tab, validating the first anti-forgery token, success! The second tab, however, will no longer pass Anti-Forgery validation, as the existing token on the page no longer matches the authenticated user.
Options for AddAntiforgery
In your application’s setup code, typically found in Program.cs
, you can configure the anti-forgery options. The AddAntiforgery
method has an overload that accepts a lambda, in which you can override the default options:
Cookie.Name
- Sets the cookie’s name used to store the Anti-Forgery token. Default:.AspNetCore.Antiforgery
FormFieldName
- Sets the form field’s name for storing the Anti-Forgery token. Default:__RequestVerificationToken
HeaderName
- Sets the header’s name used to store the Anti-Forgery token. Default:null
SuppressXFrameOptionsHeader
- Determines whether theX-Frame-Options
header is suppressed. Default:false
Here’s an example that configures the cookie name, form field name, and header name:
builder.Services.AddAntiforgery(options =>
{
options.Cookie.Name = "MyAntiforgeryCookie";
options.FormFieldName = "MyAntiforgeryField";
options.HeaderName = "X-CSRF-TOKEN";
});
The default options are generally suitable for most applications, so a simple call to AddAntiforgery()
without additional configuration is often sufficient.
Adding Anti-Forgery to MVC and Razor Pages
When working with Anti-Forgery tokens, you need to ensure that a token is rendered in the view to be sent with the request and that the server can validate the token.
In an MVC or Razor Pages application, Anti-Forgery tokens are automatically included in forms generated by the FormTagHelper
. In other words, adding a <form>
when this tag helper is registered will automatically render a hidden field with Anti-Forgery information.
Some folks prefer not to use FormTagHelper
, or are using older versions of ASP.NET that may not have access to these tag helpers. To make sure the anti-forgery token hidden field is added to every form, you can add it manually using the @Html.AntiForgeryToken()
method:
<form method="post" action="/Home/Submit">
@Html.AntiForgeryToken()
<!-- form fields -->
</form>
On the server-side, Razor Pages automatically validates anti-forgery tokens. This is thanks to a convention in the framework that automatically registers the AutoValidateAntiforgeryTokenAttribute
filter. If you want this filter to be registered automatically in ASP.NET MVC Core, you can add it to the filters collection on startup:
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
On any ASP.NET MVC controller, action method, or Razor Pages model, you can override the default behavior of validating (or ignoring) Anti-Forgery tokens. Add the [RequireAntiForgeryToken]
attribute to require validation, or [IgnoreAntiForgeryToken]
to skip validation.
Here’s how to add [RequireAntiForgeryToken]
to an action method:
public class CustomerController : Controller
{
[RequireAntiforgeryToken]
[HttpPost]
public IActionResult Save(Customer customer)
{
return View();
}
}
In Razor Pages, the attribute can be added to the PageModel
:
[IgnoreAntiforgeryToken]
public class Counter : PageModel
{
// ...
}
Note that you can not apply Anti-Forgery attributes to individual handlers in Razor Pages.
Adding Anti-Forgery to Minimal APIs
In ASP.NET Core Minimal APIs, you need to manually configure and validate Anti-Forgery tokens. There are a few steps involved:
- Register the necessary services
- Make sure Anti-Forgery tokens are generated on every response, and submitted in every request
- Validate the Anti-Forgery token
Registering the Anti-Forgery services can be done in Program.cs
:
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddAntiforgery();
// ...
Once registered, the IAntiforgery
service is available through the service provider and can be used to get or store tokens.
If you’re using HTML forms to send requests to ASP.NET Core Minimal APIs, you can add a hidden form field named __RequestVerificationToken
, and set its value using the output of the Antiforgery.GetTokens(HttpContext).RequestToken
method.
In any API, you can now validate the Anti-Forgery token by injecting the IAntiforgery
and validating it against the current HttpContext
:
app.MapPost("/save-data", async (HttpContext context, IAntiforgery antiforgery) =>
{
await antiforgery.ValidateRequestAsync(context);
// ...
return Results.Ok();
});
What to do when making requests from JavaScript?
When using JavaScript to call existing ASP.NET Core endpoints with Anti-Forgery, your calling code must also include a token to complete the request successfully. For example using Angular, React, or Vue, you need to make sure you’re sending the Anti-Forgery token either as a request parameter on POST
, or as a request header.
You’ll typically want to retrieve the Anti-Forgery token from a hidden form field or from a cookie, and include it in the request headers.
Using the Anti-Forgery token from a hidden form field
When using MVC or Razor Pages, you can use @Html.AntiForgeryToken()
to generate the hidden form field:
<form id="hiddenForm" method="post">
@Html.AntiForgeryToken()
</form>
With ASP.NET Core Minimal APIs, you can generate a form that adds a hidden field named __RequestVerificationToken
, and populate it using Antiforgery.GetTokens(HttpContext).RequestToken
.
In JavaScript, you can then use the value of this hidden field and append it to the request headers (using the default header key RequestVerificationToken
):
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
const formData = new FormData(...);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
fetch('/save-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
// Handle success
console.log('Success:', data);
})
.catch((error) => {
// Handle error
console.error('Error:', error);
});
Using the Anti-Forgery token from a cookie
As an alternative to using a hidden form field to render the Anti-Forgery token in the browser, you can also make it available in a cookie and then use that later when sending requests to the server.
Here’s a snippet that generates a new Anti-Forgery token for a request, and stores it in a cookie named XSRF-TOKEN
:
var app = builder.Build();
app.Use(async (context, next) =>
{
var antiforgery = context.RequestServices.GetRequiredService<IAntiforgery>();
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
new CookieOptions { HttpOnly = false, SameSite = SameSiteMode.Strict });
await next();
});
// ...
Note that this code sets the cookie’s HttpOnly
property to false
, to make sure the JavaScript code running in the browser can read the value stored.
In JavaScript, you can then use the value of this cookie, and append it to the request headers (using the default header key RequestVerificationToken
):
const token = document.cookie.match('(^|;)\\s*XSRF-TOKEN\\s*=\\s*([^;]+)')?.pop() || '';
fetch('/save-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: // ...
})
.then(response => response.json())
.then(data => {
// Handle success
console.log('Success:', data);
})
.catch((error) => {
// Handle error
console.error('Error:', error);
});
Anti-Forgery tokens and Duende Backend-for-Frontend (BFF)
When you deploy Duende Backend-for-Frontend (BFF), anti-forgery is enabled by default.
Anti-forgery in BFF is implemented differently from ASP.NET Core: it only requires the presence of a X-CSRF: 1
header (customizable in settings). Any React, Angular, Vue, Blazor or other front-end can easily add such header!
Combined with BFF’s cookie requirements, having this header triggers a CORS preflight request in the browser for cross-origin calls. This isolates the caller to the same origin as the backend, providing robust CSRF protection. For more information, see protection against CSRF attacks in the BFF documentation.
Load-balanced scenarios and DataProtection
In a load-balanced scenario, you must ensure that the Anti-Forgery tokens are consistent across all application instances. This requires configuring DataProtection to use a shared key storage mechanism, such as a database, Azure Blob Storage, or Azure Key Vault.
Example configuration for DataProtection in Program.cs
:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys"))
.SetApplicationName("MyApp");
Note that the application name configured here will also be used in the Anti-Forgery cookie.
Summary
Anti-Forgery tokens are a crucial part of securing your ASP.NET Core applications against CSRF attacks. By understanding the defaults and knowing how to configure and implement Anti-Forgery tokens in MVC, Razor Pages, and Minimal APIs, you can ensure that your applications are protected.
Additionally, handling Anti-Forgery tokens in AJAX requests is essential for maintaining security in dynamic web applications. In load-balanced scenarios, configuring DataProtection to use a shared key storage mechanism is necessary to ensure consistency.
Thoughts? Questions? Let us know in the comments!