Start Debugging

Solución: The antiforgery token could not be decrypted en ASP.NET Core

El error significa que Data Protection perdió la clave que firmó el token. Persiste las claves en un almacén compartido y duradero y llama a SetApplicationName para que cada instancia lea el mismo conjunto de claves.

The antiforgery token could not be decrypted porque la clave de Data Protection que lo firmó ya no está en el conjunto de claves que tu aplicación está leyendo. De forma predeterminada, ASP.NET Core escribe esas claves en una carpeta por máquina y por usuario que no sobrevive al reinicio de un contenedor y no se comparte entre instancias. Persiste las claves en un almacén duradero que toda instancia pueda leer (recurso compartido de archivos, base de datos, Azure Blob, Redis) y fija SetApplicationName con el mismo valor en todas partes. Esa es la solución completa. El resto de este artículo explica por qué ocurre y el código exacto para cada almacén.

Esto aplica a ASP.NET Core 11 (.NET 11), pero el mismo stack de Data Protection y la misma solución se remontan a ASP.NET Core 2.x.

El error en contexto

Verás uno de estos, normalmente en un POST justo después de un despliegue, un escalado horizontal o el reinicio de un contenedor:

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)

La excepción interna es la pista clave. The key {guid} was not found in the key ring significa que el token hace referencia a una clave que esta instancia nunca ha visto. Un mensaje interno distinto, The payload was invalid, apunta a una clave que existe pero no puede descifrar este token concreto, lo que normalmente significa que dos aplicaciones comparten un conjunto de claves sin un nombre de aplicación coincidente (más sobre esto abajo).

En Blazor y Razor Pages la misma causa raíz se manifiesta como un HTTP 400 con Bad Request - antiforgery token validation failed, y en MVC como un bucle de redirección en el inicio de sesión porque la cookie de antiforgery nunca puede validarse.

Por qué ocurre

Los tokens de antiforgery son cifrados y firmados por ASP.NET Core Data Protection. La validación solo funciona si la instancia que lee el token todavía conserva la clave que lo escribió. Hay cuatro formas de perder esa clave:

La vida útil predeterminada de las claves es de 90 días, así que la rotación legítima de claves casi nunca causa esto. Si lo ves, la clave falta, no ha expirado.

Reproducción mínima

Esta es la aplicación más pequeña que reproduce el error a través de un reinicio. Ejecútala, envía el formulario y luego reinicia el proceso antes de enviarlo de nuevo.

// .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();

Carga /, copia el formulario renderizado, reinicia la aplicación y luego haz POST del formulario antiguo. Como el conjunto de claves en memoria se regeneró en el reinicio, el token ya no se descifra y obtienes AntiforgeryValidationException. En un único proceso de larga duración no lo verás, que es exactamente por qué esto se escapa en las pruebas locales y solo aparece en contenedores y granjas.

La solución, en detalle

Elige el almacén que coincida con dónde ejecutas. En todos los casos la regla es la misma: una ubicación que todas las instancias puedan leer y escribir, más un nombre de aplicación estable.

1. Recurso compartido de archivos (UNC o volumen montado): lo más simple para VMs y hardware dedicado

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

En Kubernetes, monta un PersistentVolume compartido (ReadWriteMany) y apunta PersistKeysToFileSystem a la ruta de montaje. Las claves entonces sobreviven a cualquier pod individual. Esta es la solución de menor fricción cuando ya tienes almacenamiento compartido.

2. Base de datos vía EF Core: sin infraestructura extra si ya tienes una base de datos

Agrega el paquete Microsoft.AspNetCore.DataProtection.EntityFrameworkCore y un contexto que implemente IDataProtectionKeyContext:

// .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");

Ejecuta una migración que cree la tabla DataProtectionKeys una vez, y cada instancia leerá el mismo conjunto. Esta es la opción a la que recurro más a menudo porque no necesita nada que la aplicación no tenga ya.

3. Azure Blob Storage: la opción limpia para App Service y Azure Container Apps

Agrega Azure.Extensions.AspNetCore.DataProtection.Blobs. Usa una identidad administrada en lugar de una cadena de conexión:

// .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");

Esto también soluciona la variante de slots de despliegue: cuando staging y producción apuntan al mismo blob, intercambiar slots ya no invalida las sesiones activas. Para defensa en profundidad, envuélvelo con ProtectKeysWithAzureKeyVault (paquete Azure.Extensions.AspNetCore.DataProtection.Keys) para que el propio conjunto de claves esté cifrado en reposo.

4. Redis: la menor latencia para granjas grandes

Agrega Microsoft.AspNetCore.DataProtection.StackExchangeRedis y reutiliza el multiplexor que ya usas para caché:

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

Sea cual sea el almacén que elijas, SetApplicationName no es opcional en un escenario de múltiples aplicaciones o slots. Establece el discriminador de aplicación que Data Protection mezcla en la derivación de la clave, y el valor debe ser idéntico byte a byte en cada instancia y slot que deba confiar en los tokens de los demás.

Detalles y variantes

The payload was invalid en lugar de key not found. La clave existe pero el discriminador de aplicación difiere. Dos aplicaciones comparten un almacén, y a una le falta SetApplicationName o tiene un valor distinto. Establece el mismo nombre en ambas. Esto también es por qué copiar una aplicación publicada a una nueva ruta de raíz de contenido puede romper los tokens: ASP.NET Core recurre a derivar el nombre de la aplicación de la ruta de raíz de contenido cuando no estableces uno explícitamente, así que la propia ruta se convierte en el discriminador.

Claves cifradas en reposo que otras instancias no pueden leer. En Windows, Data Protection cifra el conjunto de claves con DPAPI vinculado al usuario o a la máquina actual de forma predeterminada. Una clave escrita bajo una identidad es ilegible bajo otra, lo que produce el mismo síntoma aunque las claves estén físicamente presentes. Cuando compartes claves entre máquinas, o bien desactiva el cifrado por máquina, o bien usa un protector explícito y portátil como ProtectKeysWithCertificate.

Funciona en local, falla en el contenedor. Es lo esperado. Un único proceso en tu máquina de desarrollo mantiene su conjunto efímero en memoria durante toda la sesión, así que el token siempre se descifra. El error solo aparece cuando entra en escena una segunda instancia o un reinicio. Reprodúcelo ejecutando dos instancias localmente en puertos distintos sin nada delante, o reiniciando entre solicitudes como en la reproducción de arriba.

No lo “soluciones” desactivando antiforgery. Quitar [ValidateAntiForgeryToken] o .RequireAntiforgery() hace desaparecer el error y reabre un agujero de CSRF. El token está haciendo su trabajo. Persiste las claves en su lugar.

DisableAutomaticKeyGeneration en réplicas de solo lectura. Si ejecutas un worker que debe consumir claves pero nunca acuñarlas, llama a DisableAutomaticKeyGeneration() para que no cree una clave competidora en el conjunto compartido. Déjalo desactivado en la instancia que es dueña de la rotación.

Lecturas relacionadas

Fuentes

Comments

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

< Volver