MintPlayer.AspNetCore.SpaServices.Xsrf 10.7.1

dotnet add package MintPlayer.AspNetCore.SpaServices.Xsrf --version 10.7.1
                    
NuGet\Install-Package MintPlayer.AspNetCore.SpaServices.Xsrf -Version 10.7.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MintPlayer.AspNetCore.SpaServices.Xsrf" Version="10.7.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MintPlayer.AspNetCore.SpaServices.Xsrf" Version="10.7.1" />
                    
Directory.Packages.props
<PackageReference Include="MintPlayer.AspNetCore.SpaServices.Xsrf" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MintPlayer.AspNetCore.SpaServices.Xsrf --version 10.7.1
                    
#r "nuget: MintPlayer.AspNetCore.SpaServices.Xsrf, 10.7.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MintPlayer.AspNetCore.SpaServices.Xsrf@10.7.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MintPlayer.AspNetCore.SpaServices.Xsrf&version=10.7.1
                    
Install as a Cake Addin
#tool nuget:?package=MintPlayer.AspNetCore.SpaServices.Xsrf&version=10.7.1
                    
Install as a Cake Tool

MintPlayer.AspNetCore.SpaServices.Xsrf

NuGet Version NuGet License

ASP.NET Core's antiforgery system expects a server-rendered form or view to emit the request token. A single-page application has neither, so the token never reaches the browser and every [ValidateAntiForgeryToken] endpoint rejects the SPA's requests. This package closes that gap with one middleware: on every response it obtains an antiforgery token pair and writes the request token to a JavaScript-readable XSRF-TOKEN cookie — exactly the cookie Angular's HttpClient looks for — so the SPA sends it back as a header and normal antiforgery validation just works.

Installation

NuGet Package Manager

Install-Package MintPlayer.AspNetCore.SpaServices.Xsrf

.NET CLI

dotnet add package MintPlayer.AspNetCore.SpaServices.Xsrf

How it works, end to end

  1. You register ASP.NET Core's antiforgery services and tell them which request header carries the token: services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN").
  2. You add this package's middleware with app.UseAntiforgeryGenerator().
  3. On each request the middleware hooks Response.OnStarting. Just before response headers are sent it calls IAntiforgery.GetAndStoreTokens(httpContext), which produces a token pair: a cookie token (written by the antiforgery system itself into its own HttpOnly cookie) and a request token.
  4. The middleware appends the request token to a second cookie: name XSRF-TOKEN, Path=/, and — importantly — HttpOnly = false, so client-side JavaScript can read it. That is the whole point: an HttpOnly cookie would be invisible to the SPA.
  5. The SPA reads that cookie and echoes it in the configured header. Angular's HttpClient does this automatically for same-origin mutating requests.
  6. Your endpoints validate as usual with [ValidateAntiForgeryToken], [AutoValidateAntiforgeryToken], or a global filter. Validation compares the header's request token against the antiforgery cookie token; both must be present and must match.

The middleware only issues tokens. It never validates and never rejects a request — validation stays entirely with the framework's antiforgery attributes and filters.

Public surface

The package exposes exactly one public API:

Member Description
AntiforgeryExtensions.UseAntiforgeryGenerator(this IApplicationBuilder builder) Adds the middleware that generates an XSRF token for the current user and stores it in a cookie named XSRF-TOKEN.

There is no AddXsrf() or other service-registration helper — the services you need are the framework's own AddAntiforgery(). The middleware resolves IAntiforgery from DI, so AddAntiforgery() (or something that includes it, such as AddControllersWithViews()/AddMvc()/AddRazorPages()) must be registered, otherwise the pipeline throws on the first request.

Name Value Configurable?
Request-token cookie written by this middleware XSRF-TOKEN No. The name, Path=/ and HttpOnly=false are fixed in the middleware.
Request header read during validation X-XSRF-TOKEN by convention YesAntiforgeryOptions.HeaderName. It has no default; you must set it, or header-based validation won't be attempted at all.
Antiforgery's own cookie token cookie .AspNetCore.Antiforgery.<hash> Yes — AntiforgeryOptions.Cookie. This is not the cookie the SPA reads.

Two consequences worth internalising:

  • options.Cookie.Name does not rename the SPA-visible cookie. AntiforgeryOptions.Cookie configures the framework's internal cookie-token cookie. This middleware always writes XSRF-TOKEN. If you set options.Cookie.Name = "CUSTOM-XSRF-TOKEN" and then configure your SPA to read CUSTOM-XSRF-TOKEN, the SPA will read the wrong (and HttpOnly) cookie and validation will fail.
  • So the only name you can change is the header. Change AntiforgeryOptions.HeaderName and the matching headerName in your SPA; leave the cookie name as XSRF-TOKEN on both sides.

Because the values happen to be Angular's defaults, an Angular app usually needs no client configuration at all.

Complete example

Program.cs (minimal hosting)

using MintPlayer.AspNetCore.SpaServices.Xsrf;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// The header the SPA will send the request token in. Required.
builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

// Issue the XSRF-TOKEN cookie on every response.
// Place it before the endpoints that will be validated.
app.UseAntiforgeryGenerator();

app.UseStaticFiles();
app.UseRouting();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

app.Run();

Startup.cs (classic hosting)

using MintPlayer.AspNetCore.SpaServices.Xsrf;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseAntiforgeryGenerator();

        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller}/{action=Index}/{id?}");
        });
    }
}

A validated endpoint

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    [HttpGet]
    public IEnumerable<WeatherForecast> Get() => WeatherForecast.Sample();

    [HttpPost]
    [ValidateAntiForgeryToken]   // requires a matching X-XSRF-TOKEN header
    public ActionResult CreateWeatherForecast() => Ok();
}

Validate only mutating verbs. GET/HEAD/OPTIONS/TRACE are not validated by [AutoValidateAntiforgeryToken], and putting [ValidateAntiForgeryToken] on a GET action is a good way to break your own app.

Angular: standalone bootstrap (app.config.ts)

XSRF protection is on by default in Angular's HttpClient; withXsrfConfiguration just makes the names explicit and self-documenting.

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withXsrfConfiguration({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN'
    }))
  ]
};

Angular: NgModule bootstrap

import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule,
    HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN'
    })
  ]
})
export class AppModule { }

Angular: sending a request

Nothing special is needed in the component — the interceptor adds the header:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({ selector: 'app-create-forecast', template: '<button (click)="create()">Create</button>' })
export class CreateForecastComponent {
  private readonly http = inject(HttpClient);

  create() {
    this.http.post('/WeatherForecast', {}).subscribe({
      error: (err) => console.error(err)
    });
  }
}

Custom header name

If you change the header, change it in both places and leave the cookie name alone:

services.AddAntiforgery(options => options.HeaderName = "X-CUSTOM-XSRF");
provideHttpClient(withXsrfConfiguration({
  cookieName: 'XSRF-TOKEN',      // fixed by the middleware — do not change
  headerName: 'X-CUSTOM-XSRF'    // must match AntiforgeryOptions.HeaderName
}))

Troubleshooting

400 Bad Request: "The required antiforgery header value ... is not present"

The request reached validation with no token header.

  • Check that AntiforgeryOptions.HeaderName is set. It has no default; without it the framework doesn't look for a header at all.
  • Check that the SPA's headerName matches it exactly (Angular's default is X-XSRF-TOKEN).
  • Angular only attaches the header to relative or same-origin URLs. this.http.post('/api/x', ...) gets the header; this.http.post('https://api.example.com/x', ...) does not. If your SPA builds absolute URLs from a <base href>, keep them same-origin with the API.
  • Angular also skips GET and HEAD. A validated GET will always fail.

Both tokens arrived but don't pair up.

  • The most common cause is a renamed cookie: the SPA is reading AntiforgeryOptions.Cookie.Name instead of XSRF-TOKEN. Point it back at XSRF-TOKEN.
  • Tokens are bound to the authenticated identity. If the user signs in or out between the response that issued the cookie and the request that uses it, the pair no longer matches. Because the middleware refreshes XSRF-TOKEN on every response, a plain reload or any subsequent request fixes this — but a SPA holding a stale in-memory copy of the token will not recover, so read it from the cookie at send time (as Angular's interceptor does).
  • With multiple server instances, make sure the ASP.NET Core Data Protection keys are shared; otherwise instance B can't validate a token issued by instance A.
  • Is app.UseAntiforgeryGenerator() actually reached? Middleware placed after a terminal branch (UseStaticFiles short-circuiting for a static asset, a Map/UseSpa branch, or an endpoint that already wrote the response) never runs for that request. Register it early — right after UseHttpsRedirection() is a good spot — and before your SPA/static-file and endpoint middleware.
  • The cookie is written from a Response.OnStarting callback, so it appears only on responses that actually start. Requests aborted before headers are flushed get nothing.
  • Confirm IAntiforgery is registered (AddAntiforgery() directly, or via AddControllersWithViews()/AddMvc()/AddRazorPages()).

The middleware writes the cookie with HttpOnly = false precisely so the SPA can read it. If document.cookie doesn't show it, something else is intercepting: a reverse proxy or CDN rewriting Set-Cookie, a cookie-policy middleware, or UseCookiePolicy with a restrictive MinimumSameSitePolicy. Check the raw Set-Cookie response header in the browser's network tab.

HTTPS and SameSite

The cookie is written with Path=/ and without Secure or an explicit SameSite value, so the framework's cookie policy defaults apply.

  • Keep the SPA and the API on the same origin. XSRF-cookie-to-header only works same-origin: with SameSite=Lax (the ASP.NET Core default) the cookie isn't sent on cross-site requests, and cross-origin JavaScript can't read it either. If you genuinely must split origins, you need a CORS design with credentials, SameSite=None; Secure, and Access-Control-Allow-Headers including your token header — at which point you should reconsider whether cookie-based antiforgery is the right tool.
  • During development over http://localhost, SameSite=None cookies are rejected by browsers for lacking Secure. Prefer running the dev server over HTTPS (app.UseHttpsRedirection() plus the ASP.NET Core dev certificate) and stay on the same origin.
  • Behind a TLS-terminating proxy, configure forwarded headers so the app knows the request was HTTPS; otherwise redirect and cookie behaviour will disagree with the browser's view of the connection.

License

This project is licensed under the Apache 2.0 License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.7.1 37 9/5/2026
10.7.0 36 9/3/2026
10.2.2 94 8/27/2026
10.2.1 190 1/19/2026
10.2.0 151 1/18/2026
10.1.0 146 1/16/2026
10.0.0 350 11/13/2025
10.0.0-rc.6 267 11/11/2025
10.0.0-rc.5 263 11/11/2025
10.0.0-rc.4 181 11/5/2025
10.0.0-rc.1 163 10/2/2025
9.0.0 252 11/13/2024
8.3.0 1,315 6/10/2024
8.2.0 354 11/28/2023
8.1.0 218 11/28/2023
8.0.0 260 11/18/2023

v 10.2.2
DOCUMENTATION
- The README told you to call app.UseSpaXsrf(), which does not exist. The method is
app.UseAntiforgeryGenerator().
- The README claimed support for custom header and cookie names. The cookie name is XSRF-TOKEN and
is not configurable; the header name is set through the standard services.AddAntiforgery(...)
options, not by this package.