Start Debugging

Solução: The antiforgery token could not be decrypted no ASP.NET Core

O erro significa que o Data Protection perdeu a chave que assinou o token. Persista as chaves em um armazenamento compartilhado e durável e chame SetApplicationName para que cada instância leia o mesmo conjunto de chaves.

The antiforgery token could not be decrypted porque a chave do Data Protection que o assinou não está mais no conjunto de chaves que sua aplicação está lendo. Por padrão, o ASP.NET Core grava essas chaves em uma pasta por máquina e por usuário que não sobrevive ao reinício de um contêiner e não é compartilhada entre instâncias. Persista as chaves em um armazenamento durável que toda instância possa ler (compartilhamento de arquivos, banco de dados, Azure Blob, Redis) e fixe SetApplicationName com o mesmo valor em todos os lugares. Essa é a solução completa. O resto deste artigo explica por que isso acontece e o código exato para cada armazenamento.

Isto se aplica ao ASP.NET Core 11 (.NET 11), mas o mesmo stack do Data Protection e a mesma solução remontam ao ASP.NET Core 2.x.

O erro em contexto

Você verá um destes, geralmente em um POST logo após um deploy, um escalonamento horizontal ou o reinício de um contêiner:

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)

A exceção interna é a pista. The key {guid} was not found in the key ring significa que o token referencia uma chave que esta instância nunca viu. Uma mensagem interna diferente, The payload was invalid, aponta para uma chave que existe mas não consegue descriptografar este token específico, o que geralmente significa que duas aplicações compartilham um conjunto de chaves sem um nome de aplicação correspondente (mais sobre isso abaixo).

No Blazor e no Razor Pages a mesma causa raiz se manifesta como um HTTP 400 com Bad Request - antiforgery token validation failed, e no MVC como um loop de redirecionamento no login porque o cookie de antiforgery nunca pode ser validado.

Por que isso acontece

Os tokens de antiforgery são criptografados e assinados pelo ASP.NET Core Data Protection. A validação só funciona se a instância que lê o token ainda mantiver a chave que o gravou. Há quatro formas de perder essa chave:

O tempo de vida padrão das chaves é de 90 dias, então a rotação legítima de chaves quase nunca causa isso. Se você está vendo, a chave está faltando, não expirou.

Reprodução mínima

Esta é a menor aplicação que reproduz o erro através de um reinício. Execute-a, envie o formulário e então reinicie o processo antes de enviá-lo de novo.

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

Carregue /, copie o formulário renderizado, reinicie a aplicação e então faça POST do formulário antigo. Como o conjunto de chaves em memória foi regenerado no reinício, o token não descriptografa mais e você recebe AntiforgeryValidationException. Em um único processo de longa duração você não verá, que é exatamente por que isso escapa nos testes locais e só aparece em contêineres e farms.

A solução, em detalhe

Escolha o armazenamento que combina com onde você executa. Em todos os casos a regra é a mesma: um local que todas as instâncias possam ler e gravar, mais um nome de aplicação estável.

1. Compartilhamento de arquivos (UNC ou volume montado): o mais simples para VMs e bare metal

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

No Kubernetes, monte um PersistentVolume compartilhado (ReadWriteMany) e aponte PersistKeysToFileSystem para o caminho de montagem. As chaves então sobrevivem a qualquer pod individual. Esta é a solução de menor atrito quando você já tem armazenamento compartilhado.

2. Banco de dados via EF Core: sem infraestrutura extra se você já tem um banco

Adicione o pacote Microsoft.AspNetCore.DataProtection.EntityFrameworkCore e um 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");

Execute uma migração que crie a tabela DataProtectionKeys uma vez, e cada instância lerá o mesmo conjunto. Esta é a opção à qual eu recorro com mais frequência porque não precisa de nada que a aplicação já não tenha.

3. Azure Blob Storage: a escolha limpa para App Service e Azure Container Apps

Adicione Azure.Extensions.AspNetCore.DataProtection.Blobs. Use uma identidade gerenciada em vez de uma string de conexão:

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

Isto também resolve a variante de slots de deploy: quando staging e produção apontam para o mesmo blob, trocar slots não invalida mais as sessões ativas. Para defesa em profundidade, envolva-o com ProtectKeysWithAzureKeyVault (pacote Azure.Extensions.AspNetCore.DataProtection.Keys) para que o próprio conjunto de chaves fique criptografado em repouso.

4. Redis: a menor latência para farms grandes

Adicione Microsoft.AspNetCore.DataProtection.StackExchangeRedis e reutilize o multiplexador que você já usa para cache:

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

Seja qual for o armazenamento que você escolher, SetApplicationName não é opcional em um cenário de múltiplas aplicações ou slots. Ele define o discriminador de aplicação que o Data Protection mistura na derivação da chave, e o valor deve ser idêntico byte a byte em cada instância e slot que devem confiar nos tokens uns dos outros.

Detalhes e variantes

The payload was invalid em vez de key not found. A chave existe mas o discriminador de aplicação difere. Duas aplicações compartilham um armazenamento, e uma está sem SetApplicationName ou tem um valor diferente. Defina o mesmo nome em ambas. Isto também é por que copiar uma aplicação publicada para um novo caminho de raiz de conteúdo pode quebrar os tokens: o ASP.NET Core recorre a derivar o nome da aplicação do caminho de raiz de conteúdo quando você não define um explicitamente, então o próprio caminho se torna o discriminador.

Chaves criptografadas em repouso que outras instâncias não conseguem ler. No Windows, o Data Protection criptografa o conjunto de chaves com DPAPI vinculado ao usuário ou à máquina atual por padrão. Uma chave gravada sob uma identidade é ilegível sob outra, o que produz o mesmo sintoma mesmo que as chaves estejam fisicamente presentes. Quando você compartilha chaves entre máquinas, ou desative a criptografia por máquina, ou use um protetor explícito e portátil como ProtectKeysWithCertificate.

Funciona localmente, falha no contêiner. Esperado. Um único processo na sua máquina de desenvolvimento mantém seu conjunto efêmero em memória pela sessão inteira, então o token sempre descriptografa. O bug só aparece quando uma segunda instância ou um reinício entra em cena. Reproduza-o rodando duas instâncias localmente em portas diferentes sem nada na frente, ou reiniciando entre requisições como na reprodução acima.

Não “resolva” desabilitando antiforgery. Remover [ValidateAntiForgeryToken] ou .RequireAntiforgery() faz o erro desaparecer e reabre um buraco de CSRF. O token está fazendo seu trabalho. Persista as chaves em vez disso.

DisableAutomaticKeyGeneration em réplicas somente leitura. Se você executa um worker que deve consumir chaves mas nunca cunhá-las, chame DisableAutomaticKeyGeneration() para que ele não crie uma chave concorrente no conjunto compartilhado. Deixe-o desativado na instância que é dona da rotação.

Leitura relacionada

Fontes

Comments

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

< Voltar