Persist ASP.NET Core Data Protection keys in a storage backend of your choice.
The base package lets you plug any backend into the Data Protection key ring by implementing a single, small interface. Ready-to-use SQL Server and PostgreSQL implementations (built on Dapper) are provided as separate packages.
- Why
- Packages
- Requirements
- Installation
- Quick start
- Configuration
- Database schema
- Custom storage backend
- How it works
- Contributing
- License
When an ASP.NET Core app runs on more than one node (containers, a web farm, autoscaling), every instance must share the same Data Protection keys; otherwise cookies, antiforgery tokens and anything else protected on one node cannot be read on another. This library keeps that key ring in a shared, durable store instead of the local filesystem.
| Package | Description |
|---|---|
AspNetCore.DataProtection.CustomStorage |
Core abstractions. Bring your own backend by implementing IDataProtectionStorage. |
AspNetCore.DataProtection.CustomStorage.Dapper |
Shared Dapper-based building blocks used by the database providers. |
AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer |
Ready-to-use SQL Server provider. |
AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL |
Ready-to-use PostgreSQL provider. |
The database provider packages transitively depend on .Dapper and the core package, so installing a provider is all you need for the SQL Server or PostgreSQL scenario.
- .NET 10.0 or later
For SQL Server:
dotnet add package AspNetCore.DataProtection.CustomStorage.Dapper.SQLServerFor PostgreSQL:
dotnet add package AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQLFor a custom backend, install only the core package:
dotnet add package AspNetCore.DataProtection.CustomStorageusing AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection()
.PersistKeysWithDapperInSqlServer(
builder.Configuration.GetConnectionString("DataProtection")!);
var app = builder.Build();
// Creates the keys table if it does not exist yet
// (InitializeTable defaults to true; see Configuration below).
app.Services.UseDapperDataProtection();
app.Run();using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection()
.PersistKeysWithDapperInPostgreSQL(
builder.Configuration.GetConnectionString("DataProtection")!);
var app = builder.Build();
app.Services.UseDapperDataProtection();
app.Run();Note: the PostgreSQL provider enables Dapper's snake_case column mapping globally (
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true). Keep this in mind if the same application uses Dapper elsewhere.
Both providers accept an optional Action<DapperDataProtectionConfig> to override the defaults:
builder.Services.AddDataProtection()
.PersistKeysWithDapperInSqlServer(connectionString, config =>
{
config.SchemaName = "security";
config.TableName = "DpKeys";
config.InitializeTable = false; // you manage the schema yourself
});| Option | Type | Default (SQL Server) | Default (PostgreSQL) | Description |
|---|---|---|---|---|
SchemaName |
string |
dbo |
public |
Schema that contains the keys table. |
TableName |
string |
DataProtectionKeys |
data_protection_keys |
Name of the keys table. |
InitializeTable |
bool |
true |
true |
When true, UseDapperDataProtection() creates the table (and its unique index) if it is missing. |
UseDapperDataProtection() is an extension on IServiceProvider. Call it once at startup
(app.Services.UseDapperDataProtection()): it validates the service registrations and, when
InitializeTable is true, creates the table. If you set InitializeTable = false you are
responsible for creating the schema (see below) before the app stores keys.
When InitializeTable is enabled, the provider creates a table equivalent to the following.
SQL Server (dbo.DataProtectionKeys by default):
CREATE TABLE [dbo].[DataProtectionKeys](
[Id] [int] IDENTITY(1,1) NOT NULL,
[InsertDate] [datetime] NOT NULL DEFAULT getdate(),
[FriendlyName] [nvarchar](256) NULL,
[Xml] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_DataProtectionKeys] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE UNIQUE NONCLUSTERED INDEX [IX_DataProtectionKeys_FriendlyName]
ON [dbo].[DataProtectionKeys]([FriendlyName] ASC)
WHERE [FriendlyName] IS NOT NULL;PostgreSQL (public.data_protection_keys by default):
CREATE TABLE IF NOT EXISTS public.data_protection_keys (
id INTEGER GENERATED ALWAYS AS IDENTITY,
insert_date timestamp with time zone NOT NULL DEFAULT NOW(),
friendly_name character varying(256) NULL,
xml text NOT NULL,
CONSTRAINT pk_public_data_protection_keys PRIMARY KEY (id)
);
CREATE UNIQUE INDEX IF NOT EXISTS ix_public_data_protection_keys_friendly_name
ON public.data_protection_keys (friendly_name ASC NULLS LAST);The unique index on FriendlyName / friendly_name allows NULL values.
Reference AspNetCore.DataProtection.CustomStorage and implement IDataProtectionStorage:
using AspNetCore.DataProtection.CustomStorage;
public class MyDataProtectionStorage : IDataProtectionStorage
{
public IEnumerable<DataProtectionKey> GetAll()
{
// load every stored key from your backend
}
public void Insert(DataProtectionKey key)
{
// persist a new key; key.FriendlyName must be unique when it is not null
}
}DataProtectionKey carries the two values the key ring needs:
FriendlyName(string?) — a name that must be unique when it is notnull.Xml(string, required) — the serialized key.
Register your implementation in DI and wire it up:
builder.Services.AddScoped<MyDataProtectionStorage>();
builder.Services.AddDataProtection()
.PersistKeysToStorage<MyDataProtectionStorage>();PersistKeysToStorage<TStorage>() resolves TStorage from a fresh dependency-injection scope
on each read/write, so you must register it yourself (any lifetime works). If an Insert throws,
it is wrapped in a KeyInsertException.
PersistKeysToStorage<TStorage>() registers a StorageWrapper<TStorage> as the Data Protection
IXmlRepository. The wrapper adapts the framework's XML-element calls to the simpler
IDataProtectionStorage contract (GetAll() / Insert()), resolving TStorage from a scoped
service provider for every operation.
The database providers add a Dapper layer (IDbDataProtectionStorage) on top of that contract,
implementing the raw SQL for SQL Server and PostgreSQL, plus schema initialization
(InitializeDb()) and an async read path (GetAllAsync()).
Issues and pull requests are welcome at the GitHub repository.
Licensed under the Apache License 2.0.