Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

Understanding Anti-Forgery in ASP.NET Core

Maarten Balliauw
Two blue circles

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.

Diagram explaining Cross-Site Request Forgery (CSRF)

Example of a CSRF Attack

Here's an example of how a CSRF attack works:

  1. A user logs into their banking website (example.com) and receives an authentication cookie.
  2. Without logging out, the user visits a malicious website.
  3. The malicious site submits a hidden form to example.com/transfer, using the user's authentication cookie.
  4. 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 the X-Frame-Options header is suppressed. Default: false

Here's an example that configures the cookie name, form field name, and header name:

Csharp

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:

Csharp

<form method="post" action="/Home/Submit">
    @Html.AntiForgeryToken()
    
</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:

Csharp

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:

Csharp

public class CustomerController : Controller
{
    [RequireAntiforgeryToken]
    [HttpPost]
    public IActionResult Save(Customer customer)
    {
        return View();
    }
}

In Razor Pages, the attribute can be added to the PageModel:

Csharp

[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:

Csharp

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:

Csharp

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:

Csharp

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

Javascript

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:

Csharp

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

Javascript

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:

Csharp

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!