An anti-forgery token is a server-generated value submitted with a request to verify that it originated from the legitimate application context. It counters cross-site request forgery (CSRF), where a malicious site causes a browser to send an authenticated request carrying the victim’s session cookie. section 27.8
ASP.NET Core’s FormTagHelper automatically adds a hidden token when a form uses routing attributes such as asp-controller, asp-action, or asp-page without an action attribute. Controllers must still enable validation. section 27.8 section 27.8.1
Configure validation according to the request surface:
- Apply AutoValidateAntiforgeryToken to validate every HTTP method except GET, HEAD, OPTIONS, and TRACE. section 27.8.1
- Use IgnoreValidationToken for an action or controller that must bypass validation, or ValidateAntiForgeryToken to enforce it, including on normally exempt methods. section 27.8.1
- Test failure by removing or changing the hidden input through browser developer tools, then submitting the form. section 27.8.1
JavaScript client
For an HTML-free JavaScript client, expose the token through a readable XSRF-TOKEN cookie and accept it through the X-XSRF-TOKEN header. Client code reads the cookie and sends that value with its POST request. section 27.8.3
Listing 27.29 Enabling the anti-forgery feature in the Controllers/FormController.cs file
using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace WebApp.Controllers {
[AutoValidateAntiforgeryToken]
public class FormController : Controller {
private DataContext context;
public FormController(DataContext dbContext) {
context = dbContext;
}
public async Task<IActionResult> Index(long id = 1) {
ViewBag.Categories = new SelectList(context.Categories,
"CategoryId", "Name");
return View("Form", await context.Products
.Include(p => p.Category)
.Include(p => p.Supplier)
.FirstAsync(p => p.ProductId == id)
?? new() { Name = string.Empty });
}
[HttpPost]
public IActionResult SubmitForm() {
foreach (string key in Request.Form.Keys) {
TempData[key] = string.Join(", ",
(string?)Request.Form[key]);
}
return RedirectToAction(nameof(Results));
}
public IActionResult Results() {
return View();
}
}
}
Browse the controller example that applies AutoValidateAntiforgeryToken. section 27.8.1
Listing 27.31 Configuring the anti-forgery token in the WebApp/Program.cs file
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
using Microsoft.AspNetCore.Antiforgery;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
opts.UseSqlServer(builder.Configuration[
"ConnectionStrings:ProductConnection"]);
opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSingleton<CitiesData>();
builder.Services.Configure<AntiforgeryOptions>(opts => {
opts.HeaderName = "X-XSRF-TOKEN";
});
var app = builder.Build();
app.UseStaticFiles();
IAntiforgery antiforgery
= app.Services.GetRequiredService<IAntiforgery>();
app.Use(async (context, next) => {
if (!context.Request.Path.StartsWithSegments("/api")) {
string? token =
antiforgery.GetAndStoreTokens(context).RequestToken;
if (token != null) {
context.Response.Cookies.Append("XSRF-TOKEN",
token,
new CookieOptions { HttpOnly = false });
}
}
await next();
});
app.MapControllerRoute("forms",
"controllers/{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
var context = app.Services.CreateScope().ServiceProvider
.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();
Browse the middleware and AntiforgeryOptions configuration for cookie-and-header clients. section 27.8.3
Listing 27.32 The contents of the JavaScriptForm.cshtml file in the Pages folder
@page "/pages/jsform"
<script type="text/javascript">
async function sendRequest() {
const token = document.cookie.replace(
/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, "$1");
let form = new FormData();
form.append("name", "Paddle");
form.append("price", 100);
form.append("categoryId", 1);
form.append("supplierId", 1);
let response = await fetch("@Url.Page("FormHandler")", {
method: "POST",
headers: { "X-XSRF-TOKEN": token },
body: form
});
document.getElementById("content").innerHTML
= await response.text();
}
document.addEventListener("DOMContentLoaded",
() => document.getElementById("submit").onclick = sendRequest);
</script>
<button class="btn btn-primary m-2" id="submit">
Submit JavaScript Form
</button>
<div id="content"></div>
Browse the JavaScript fetch example that reads the cookie and sends the token header. section 27.8.3
