⚙️ Antiforgery Validation Passes Locally – Fails Randomly Once Load-Balanced
A form submission with antiforgery token protection works reliably in local development and single-server staging, and once deployed behind a load balancer across multiple servers, users start hitting random 400 errors – ‘the antiforgery token could not be decrypted’ – despite submitting the form normally, without doing anything unusual.
🐞 The Problem
BadHttpRequestException: The antiforgery cookie token and form field token were not decrypted using the same key. // Happens intermittently - specifically when the request that // generated the form and the request that RECEIVED the submission // were served by two DIFFERENT servers behind the load balancer.
🔍 Why This Happens
ASP.NET Core's antiforgery (and Data Protection more broadly) uses encryption keys to protect the token - by default, each server instance generates and stores its OWN keys locally, with no built-in sharing between separate server processes. If server A issues a form with a token encrypted using its own key, and the form submission later lands on server B (a completely normal outcome behind a load balancer with no session affinity), server B cannot decrypt a token that was encrypted with a key it never had - producing exactly this intermittent, server-dependent failure.
✅ The Fix: Share the Data Protection Keys Across All Servers
- Configure ASP.NET Core’s Data Protection system to persist its keys to a SHARED location every server instance can read – a shared network file path, a Redis cache, Azure Blob Storage, or a shared database table – instead of each server’s own local, isolated storage.
- Set an explicit, consistent Application Name across all server instances via
SetApplicationName()– Data Protection uses this to isolate keys between different applications, and a mismatch here causes the exact same symptom even with shared key storage. - As an alternative fix specifically for antiforgery (if a full Data Protection key-sharing setup isn’t practical), enabling sticky sessions/session affinity on the load balancer ensures a user’s requests consistently hit the same server – simpler, but less resilient than proper key sharing.
📦 Sharing Data Protection Keys via a Shared Path
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\shared-server\keys"))
.SetApplicationName("MyApp"); // must match EXACTLY across all servers
A load balancer promising ‘any server can handle any request’ only holds if every server actually shares what it needs to — an antiforgery key born on one machine was never meant to be understood by another.
