Illustration unavailable: ASP.NET Core logo (original image source).
All code is illustrative and unexecuted.
CSRF exploits credentials that a browser attaches automatically, such as authentication cookies, to make unwanted requests to a trusted application. Cookies are sent according to domain, path, Secure and SameSite rules, not literally on every HTTP request. SameSite is useful defense in depth, but token validation is still appropriate for cookie-authenticated state-changing actions. XSS and overly broad cross-origin trust can defeat assumptions about who can read a token.
This example covers an existing HTTPS, cookie-authenticated ASP.NET Core MVC application. GET, HEAD, OPTIONS and TRACE must not change application state. APIs using only an explicit Authorization bearer header have a different CSRF threat model; if authentication is in a cookie, do not assume that calling it an API removes the risk.
Register services and built-in validation
AutoValidateAntiforgeryTokenAttribute validates unsafe MVC requests and skips safe methods. ValidateAntiForgeryToken is also valid: apply it to an action when that action must always have a token. The original instruction never to use it was incorrect. Service registration is AddAuthentication; request processing is UseAuthentication. Token issuance must see the authenticated identity.
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
builder.Services.AddAuthorization();
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.Name = "__Host-Antiforgery";
options.Cookie.Path = "/";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
});
builder.Services.AddControllersWithViews(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();This startup fragment assumes your application already implements its own sign-in and protected actions. Adapt cookie SameSite choices to your login flow; Strict can interfere with cross-site flows. It is not a complete sign-in system.
Issue the request token after authentication
Inject IAntiforgery (the exact interface spelling) in a controller. The internal antiforgery cookie remains HttpOnly; the separate request token can be read by same-origin JavaScript and returned in the configured header. Refresh it after sign-in or an identity change. Do not publish a cross-origin endpoint that exposes tokens to untrusted origins.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
[Authorize]
[Route("security/antiforgery")]
public sealed class AntiforgeryController : Controller
{
private readonly IAntiforgery antiforgery;
public AntiforgeryController(IAntiforgery antiforgery)
=> this.antiforgery = antiforgery;
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public IActionResult Get()
{
var tokens = antiforgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!, new CookieOptions
{
HttpOnly = false,
Secure = true,
SameSite = SameSiteMode.Strict,
Path = "/",
IsEssential = true
});
return NoContent();
}
}Send the token explicitly
Cookie and header defaults differ between client libraries and versions. Do not rely on the name alone. The following is a JavaScript code example displayed as text, not an embedded script; it prepares options for your existing same-origin mutation call.
// In your application's same-origin JavaScript bundle, after sign-in:
await fetch("/security/antiforgery", { credentials: "same-origin" });
const tokenCookie = document.cookie.split("; ")
.find(part => part.startsWith("XSRF-TOKEN="));
if (!tokenCookie) throw new Error("Missing antiforgery request token");
const requestToken = decodeURIComponent(tokenCookie.slice("XSRF-TOKEN=".length));
// For the application's existing same-origin mutation request:
const requestOptions = {
method: "POST",
credentials: "same-origin",
headers: { "X-XSRF-TOKEN": requestToken, "Content-Type": "application/json" }
};
// Attach requestOptions and the intended JSON body to that request.
// Do not send this header to a URL controlled by untrusted input.When manual validation is justified
For MVC, the built-in filters usually remove the need for a custom action filter. Outside that path, IAntiforgery.IsRequestValidAsync(HttpContext) is a supported manual check; it returns true without validating a token for GET, HEAD, OPTIONS and TRACE. Reject a false result before changing state. Do not create a new dependency-injection scope merely to resolve the request-scoped service. Antiforgery is not authorization: protected actions must also enforce the user's permissions. These snippets were not compiled or run.
Primary sources
- Microsoft ASP.NET Core antiforgery
- IAntiforgery.IsRequestValidAsync
- OWASP CSRF Prevention Cheat Sheet
- Microsoft middleware ordering
Original English article: Add CSRF protection to .NET core (Antiforgery). Original publication date: 2022-02-06T09:49:00.018+02:00.