ASP.NET Core Data Protection keys
Keep users signed in across restarts by storing the key ring in the private folder instead of DPAPI.
ASP.NET Core protects authentication cookies, antiforgery tokens and similar data with its Data Protection keys. On shared hosting the default key storage does not work, and the result is users who are signed out after every restart, antiforgery errors or even a login page that returns HTTP 500. The fix is a few lines in Program.cs.
How to recognise it
Typical entries in the logs of the website:
Using an in-memory repository. Keys will not be persisted to storage.
Neither user profile nor HKLM registry available. Using an ephemeral key repository.
No XML encryptor configured. Key {...} may be persisted to storage in unencrypted form.
System.Security.Cryptography.CryptographicException ... at ProtectWithDpapiCore
The antiforgery token could not be decrypted.
And the symptoms: everyone is logged out whenever the application restarts, forms fail with an antiforgery error after a deployment, or a page that uses IDataProtector throws an exception.
Why it happens
By default ASP.NET Core stores its keys in the Windows user profile of the account that runs the application and encrypts them with Windows DPAPI. The application pool of a hosted website runs without a Windows user profile, so neither the profile folder nor DPAPI is available. The application falls back to keys kept only in memory — new keys on every start.
Store the keys in the private folder
Every website has a private folder next to wwwroot. It is outside the public root, it is writable by your application, and deployments do not touch it. Keep the key ring there:
Configure Data Protection
In
Program.csadd:var builder = WebApplication.CreateBuilder(args); var keysPath = Path.Combine(builder.Environment.ContentRootPath, "..", "private", "keys"); builder.Services.AddDataProtection() .SetApplicationName("MyApplication") .PersistKeysToFileSystem(new DirectoryInfo(keysPath)) .SetDefaultKeyLifetime(TimeSpan.FromDays(90));ContentRootPathis thewwwrootfolder on the server, so../private/keysresolves to the private folder of the website. Do not callProtectKeysWithDpapi()orProtectKeysWithDpapiNG().Publish and restart
Publish the application. On the first start it creates
private/keys/key-*.xml. Over FTP the structure looks like this:/wwwroot your application /private /keys key-*.xmlClear old cookies
Cookies issued while the application was still using in-memory keys cannot be decrypted any more. Sign in again in a fresh browser session; the antiforgery errors disappear with the old cookies.
Note
The warning "No XML encryptor configured" remains and is expected: without DPAPI the keys are stored as plain XML. The private folder is accessible only to your own website — no other customer or process can read it — so this is not a security problem. Encrypting the keys adds complexity and brings no benefit here.
Certificates and PFX files
The same applies to certificates. The Windows certificate store is not available to your application, so do not look certificates up by thumbprint. Put the PFX file into private/certs and load it from the file:
var certPath = Path.Combine(builder.Environment.ContentRootPath, "..", "private", "certs", "signing.pfx");
var certificate = new X509Certificate2(certPath, "pfx-password",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet);
This works for IdentityServer, OpenIddict, Duende, JWT signing keys and for protecting the Data Protection keys with a certificate (ProtectKeysWithCertificate), if you need that.
Load user profile
Premium websites can switch Load user profile on: Websites → your website → Scripting → Advanced settings. The application pool then gets a Windows profile and DPAPI works. It is intended for third-party libraries that insist on a user profile; it makes the application start slower and use more resources, so the recommended setting is Disabled. Store the keys in private/keys instead.
On the Free plan the option is not available.