Start Debugging

Lösung: The antiforgery token could not be decrypted in ASP.NET Core

Der Fehler bedeutet, dass Data Protection den Schlüssel verloren hat, der das Token signiert hat. Persistieren Sie die Schlüssel in einem gemeinsamen, dauerhaften Speicher und rufen Sie SetApplicationName auf, damit jede Instanz denselben Schlüsselring liest.

The antiforgery token could not be decrypted, weil der Data-Protection-Schlüssel, der es signiert hat, nicht mehr im Schlüsselring liegt, den Ihre Anwendung liest. Standardmäßig schreibt ASP.NET Core diese Schlüssel in einen maschinen- und benutzerspezifischen Ordner, der einen Neustart eines Containers nicht übersteht und nicht zwischen Instanzen geteilt wird. Persistieren Sie die Schlüssel in einem dauerhaften Speicher, den jede Instanz lesen kann (Dateifreigabe, Datenbank, Azure Blob, Redis), und fixieren Sie SetApplicationName überall auf denselben Wert. Das ist die vollständige Lösung. Der Rest dieses Artikels erklärt, warum es passiert, und den genauen Code für jeden Speicher.

Dies gilt für ASP.NET Core 11 (.NET 11), aber derselbe Data-Protection-Stack und dieselbe Lösung reichen bis ASP.NET Core 2.x zurück.

Der Fehler im Kontext

Sie sehen einen davon, üblicherweise bei einem POST direkt nach einer Bereitstellung, einem Scale-out oder einem Container-Neustart:

Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery token could not be decrypted.
 ---> System.Security.Cryptography.CryptographicException: The key {0cf0e637-...} was not found in the key ring.
   at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(...)
   at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgeryTokenSerializer.Deserialize(String serializedToken)

Die innere Ausnahme ist der entscheidende Hinweis. The key {guid} was not found in the key ring bedeutet, dass das Token einen Schlüssel referenziert, den diese Instanz noch nie gesehen hat. Eine andere innere Meldung, The payload was invalid, deutet auf einen Schlüssel hin, der existiert, aber genau dieses Token nicht entschlüsseln kann, was üblicherweise bedeutet, dass zwei Anwendungen einen Schlüsselring ohne übereinstimmenden Anwendungsnamen teilen (mehr dazu unten).

In Blazor und Razor Pages zeigt sich dieselbe Ursache als HTTP 400 mit Bad Request - antiforgery token validation failed, und in MVC als Weiterleitungsschleife beim Login, weil das Antiforgery-Cookie niemals validiert werden kann.

Warum das passiert

Antiforgery-Token werden von ASP.NET Core Data Protection verschlüsselt und signiert. Die Validierung funktioniert nur, wenn die Instanz, die das Token liest, noch den Schlüssel hält, der es geschrieben hat. Es gibt vier Wege, diesen Schlüssel zu verlieren:

Die Standard-Lebensdauer der Schlüssel beträgt 90 Tage, sodass legitime Schlüsselrotation dies fast nie verursacht. Wenn Sie es sehen, fehlt der Schlüssel, er ist nicht abgelaufen.

Minimale Reproduktion

Dies ist die kleinste Anwendung, die den Fehler über einen Neustart hinweg reproduziert. Führen Sie sie aus, senden Sie das Formular ab und starten Sie dann den Prozess neu, bevor Sie es erneut absenden.

// .NET 11, ASP.NET Core 11
// No Data Protection configuration: keys are ephemeral.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();

app.MapGet("/", (HttpContext ctx, IAntiforgery af) =>
{
    var tokens = af.GetAndStoreTokens(ctx);
    return Results.Content($"""
        <form action="/submit" method="post">
          <input name="{tokens.FormFieldName}" type="hidden" value="{tokens.RequestToken}" />
          <button type="submit">Submit</button>
        </form>
        """, "text/html");
});

app.MapPost("/submit", () => "OK").RequireAntiforgery();

app.Run();

Laden Sie /, kopieren Sie das gerenderte Formular, starten Sie die Anwendung neu und führen Sie dann ein POST des alten Formulars aus. Da der speicherinterne Schlüsselring beim Neustart neu erzeugt wurde, entschlüsselt das Token nicht mehr und Sie erhalten AntiforgeryValidationException. In einem einzelnen, langlebigen Prozess sehen Sie es nicht, was genau der Grund ist, warum dies durch lokale Tests rutscht und erst in Containern und Farmen zubeißt.

Die Lösung im Detail

Wählen Sie den Speicher, der zu Ihrer Ausführungsumgebung passt. In jedem Fall lautet die Regel gleich: ein Ort, den alle Instanzen lesen und schreiben können, plus ein stabiler Anwendungsname.

1. Dateifreigabe (UNC oder eingehängtes Volume): am einfachsten für VMs und Bare Metal

// .NET 11, ASP.NET Core 11
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"\\fileserver\dpkeys\myapp"))
    .SetApplicationName("MyApp");

In Kubernetes hängen Sie ein gemeinsames PersistentVolume (ReadWriteMany) ein und richten PersistKeysToFileSystem auf den Einhängepfad. Die Schlüssel überleben dann jeden einzelnen Pod. Dies ist die reibungsärmste Lösung, wenn Sie bereits gemeinsamen Speicher haben.

2. Datenbank über EF Core: keine zusätzliche Infrastruktur, wenn Sie bereits eine Datenbank haben

Fügen Sie das Paket Microsoft.AspNetCore.DataProtection.EntityFrameworkCore und einen Kontext hinzu, der IDataProtectionKeyContext implementiert:

// .NET 11, EF Core 11
public class KeysDbContext : DbContext, IDataProtectionKeyContext
{
    public KeysDbContext(DbContextOptions<KeysDbContext> options) : base(options) { }
    public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
}
// Program.cs
builder.Services.AddDbContext<KeysDbContext>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Keys")));

builder.Services.AddDataProtection()
    .PersistKeysToDbContext<KeysDbContext>()
    .SetApplicationName("MyApp");

Führen Sie einmal eine Migration aus, die die Tabelle DataProtectionKeys erstellt, und jede Instanz liest denselben Ring. Dies ist die Option, zu der ich am häufigsten greife, weil sie nichts benötigt, was die Anwendung nicht ohnehin schon hat.

3. Azure Blob Storage: die saubere Wahl für App Service und Azure Container Apps

Fügen Sie Azure.Extensions.AspNetCore.DataProtection.Blobs hinzu. Verwenden Sie eine verwaltete Identität statt einer Verbindungszeichenfolge:

// .NET 11, ASP.NET Core 11
builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(
        new Uri("https://mystorage.blob.core.windows.net/dpkeys/keys.xml"),
        new DefaultAzureCredential())
    .SetApplicationName("MyApp");

Dies behebt auch die Variante mit Bereitstellungsslots: Wenn Staging und Produktion auf denselben Blob zeigen, macht ein Slot-Wechsel keine aktiven Sitzungen mehr ungültig. Für gestaffelte Sicherheit umschließen Sie es mit ProtectKeysWithAzureKeyVault (Paket Azure.Extensions.AspNetCore.DataProtection.Keys), damit der Schlüsselring selbst im Ruhezustand verschlüsselt ist.

4. Redis: die geringste Latenz für große Farmen

Fügen Sie Microsoft.AspNetCore.DataProtection.StackExchangeRedis hinzu und verwenden Sie den Multiplexer wieder, den Sie bereits für Caching nutzen:

// .NET 11, ASP.NET Core 11
var redis = ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!);
builder.Services.AddDataProtection()
    .PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys")
    .SetApplicationName("MyApp");

Welchen Speicher Sie auch wählen, SetApplicationName ist in einem Szenario mit mehreren Anwendungen oder Slots nicht optional. Es legt den Anwendungsdiskriminator fest, den Data Protection in die Schlüsselableitung einmischt, und der Wert muss byteweise identisch über jede Instanz und jeden Slot sein, die den Token der jeweils anderen vertrauen sollen.

Fallstricke und Varianten

The payload was invalid statt key not found. Der Schlüssel existiert, aber der Anwendungsdiskriminator weicht ab. Zwei Anwendungen teilen einen Speicher, und einer fehlt SetApplicationName oder sie hat einen anderen Wert. Setzen Sie in beiden denselben Namen. Dies ist auch der Grund, warum das Kopieren einer veröffentlichten Anwendung an einen neuen Content-Root-Pfad die Token brechen kann: ASP.NET Core leitet den Anwendungsnamen aus dem Content-Root-Pfad ab, wenn Sie keinen explizit setzen, sodass der Pfad selbst zum Diskriminator wird.

Im Ruhezustand verschlüsselte Schlüssel, die andere Instanzen nicht lesen können. Unter Windows verschlüsselt Data Protection den Schlüsselring standardmäßig mit DPAPI, gebunden an den aktuellen Benutzer oder die aktuelle Maschine. Ein unter einer Identität geschriebener Schlüssel ist unter einer anderen nicht lesbar, was dasselbe Symptom erzeugt, obwohl die Schlüssel physisch vorhanden sind. Wenn Sie Schlüssel über Maschinen hinweg teilen, deaktivieren Sie entweder die maschinengebundene Verschlüsselung oder verwenden Sie einen expliziten, portablen Schutz wie ProtectKeysWithCertificate.

Es funktioniert lokal, scheitert im Container. Erwartungsgemäß. Ein einzelner Prozess auf Ihrer Entwicklungsmaschine hält seinen flüchtigen Ring für die gesamte Sitzung im Speicher, sodass das Token immer entschlüsselt. Der Fehler tritt erst auf, sobald eine zweite Instanz oder ein Neustart ins Spiel kommt. Reproduzieren Sie ihn, indem Sie zwei Instanzen lokal auf verschiedenen Ports ohne etwas davor ausführen oder zwischen Anfragen neu starten, wie in der obigen Reproduktion.

„Beheben” Sie es nicht durch Deaktivieren von Antiforgery. Das Entfernen von [ValidateAntiForgeryToken] oder .RequireAntiforgery() lässt den Fehler verschwinden und öffnet ein CSRF-Loch erneut. Das Token tut seine Arbeit. Persistieren Sie stattdessen die Schlüssel.

DisableAutomaticKeyGeneration auf schreibgeschützten Replikaten. Wenn Sie einen Worker betreiben, der Schlüssel konsumieren, aber niemals erzeugen soll, rufen Sie DisableAutomaticKeyGeneration() auf, damit er keinen konkurrierenden Schlüssel im gemeinsamen Ring erstellt. Lassen Sie es auf der Instanz aus, die die Rotation besitzt.

Verwandte Lektüre

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück