Antiforgery & Security Headers
Two things a server that answers a browser needs, and an API-only server mostly does not.
Antiforgery
Section titled “Antiforgery”builder.AddAntiforgery();app.UseAntiforgery();The page (or the app’s bootstrap data) gets the token pair:
app.MapGet("/", ctx =>{ var tokens = ctx.GetRequiredService<IAntiforgery>().GetTokens(ctx);
// tokens.CookieToken is already on the response; echo tokens.RequestToken back in the header. return ctx.Response.WriteTextAsync(Render(tokens.RequestToken), "text/html");});and sends it back on unsafe requests:
fetch('/api/things', { method: 'POST', headers: { 'X-CSRF-TOKEN': token }, body: JSON.stringify(thing)});The rule that decides when the check applies
Section titled “The rule that decides when the check applies”Unsafe methods — POST, PUT, PATCH, DELETE — are checked when the request carries cookies, and skipped when it does not.
That is not a shortcut. CSRF is an attack on ambient credentials: a cookie the browser attaches whether or not the page meant to. A caller holding a bearer token has to attach it deliberately, and an attacker’s page cannot. Checking those would cost every API client a token exchange to prevent an attack that cannot happen to them.
Override it either way:
app.MapPost("/sensitive", Handler).ValidateAntiforgery(); // check it regardlessapp.MapPost("/webhook", Handler).DisableAntiforgery(); // never check itor with [ValidateAntiforgery] / [DisableAntiforgery] on a typed endpoint.
How the token works
Section titled “How the token works”Signed double-submit. The cookie carries a random value; the request token is a timestamp plus an HMAC over both. An attacker’s page can cause the cookie to be sent — that is what CSRF is — but cannot read it and cannot forge the HMAC, so it cannot produce a matching request token. Comparison is fixed-time.
The cookie is deliberately not HttpOnly: a single-page app has to read the pair to echo it
back, and the security comes from the signature rather than from secrecy of the cookie value.
| Property | Default | Notes |
|---|---|---|
CookieName |
shiny.antiforgery |
|
HeaderName |
X-CSRF-TOKEN |
Where the request token is read from |
FormFieldName |
__RequestVerificationToken |
For a posted form — see below |
Key |
generated at startup | Set it to survive a restart, or to share across two servers |
SecureCookie |
false |
A LAN server is usually cleartext |
Lifetime |
8 hours | Inside the signature, so an expired token cannot be edited |
The token is read from the header, not the form body: reading a form would consume the request body before the handler could. For a page that posts a real form, read the form yourself and validate what it carried:
var form = await ctx.Request.ReadFormAsync(ctx.RequestAborted);
if (!antiforgery.ValidateToken(ctx, form["__RequestVerificationToken"])){ ctx.Response.StatusCode = 400; return;}Security headers
Section titled “Security headers”app.UseSecurityHeaders();| Header | Default |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
no-referrer |
Cross-Origin-Resource-Policy |
same-origin |
Content-Security-Policy |
not set |
Permissions-Policy |
not set |
Cross-Origin-Opener-Policy |
not set |
Strict-Transport-Security |
not set |
Register it early: the headers are applied as the response starts, so they cover static files and error responses too — exactly the ones no handler sets headers on. A handler that set its own keeps it; these are defaults, not overrides.
CSP is not set by default because a wrong one breaks a working page and only the app knows what it loads. For a UI that ships with the app there is a starting point:
app.UseSecurityHeaders(o => o.ContentSecurityPolicy = SecurityHeaderOptions.SelfOnlyContentSecurityPolicy);// default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'app.UseSecurityHeaders(o => o.Hsts = new HstsOptions{ MaxAge = TimeSpan.FromDays(365), IncludeSubDomains = true});It is only emitted over HTTPS in any case: a browser is entitled to ignore it on a cleartext connection, and a proxy that is not is one downgrade away from doing damage.
HTTPS redirect
Section titled “HTTPS redirect”app.UseHttpsRedirection(); // port from the first TLS endpoint configuredapp.UseHttpsRedirection(httpsPort: 8443);A 307, so the method and body survive. With nothing to redirect to, the request is served rather than sent in a circle back to the endpoint it is already on.


