.NET Core SQL DataProtection Key Storage Provider using Entity Framework

Home / .NET Core SQL DataProtection Key Storage Provider using Entity Framework

Using .NET Core Data Protection is a bit limited. I like how it generates keys and can maintain them, but the storage mechanisms out of the box are fairly limited. Unless you’re using Redis or Azure Stoage, your only option is file system persistence. This isn’t really usable for distributed applications that need to share keys. Ideally, using a SQL server backend would be available, but it’s not too terribly difficult to create one.


To implement a key storage provider, the IXmlRepository interface must be used. This interface is pretty simple. It only has two methods and each of its objects only carry two pieces of information which act as key value pairs. Thinking of this in SQL terms, we can create our table immediately:

CREATE TABLE [dbo].[DataProtectionKeys](
	[FriendlyName] [nvarchar](max) NOT NULL,
	[XmlData] [nvarchar](max) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

I’m going to create a model, mappings, repository, database context, and extensions.

Our model, DataProtectionKey, then looks equally the same:

public class DataProtectionKey
{
    public string FriendlyName { get; set; }
    public string XmlData { get; set; }
}

The fluent mappings, using the EntityTypeBuilder, which I’ve blogged about previously look like this:

internal class DataProtectionKeyConfiguration : DbEntityConfiguration<DataProtectionKey>
{
    public override void Configure(EntityTypeBuilder<DataProtectionKey> entity)
    {
        entity.ToTable("DataProtectionKeys", DataProtectionContext.DatabaseSchema);

        entity.HasKey(x => x.FriendlyName);
        entity.Property(p => p.FriendlyName).HasColumnName("FriendlyName").HasColumnType("nvarchar(max)");

        entity.Property(p => p.XmlData).HasColumnName("XmlData").HasColumnType("nvarchar(max)");
    }
}

After defining our schema and objects, we need a DataProtectionContext to tie it together. I use my already implemented BaseContext, but there is nothing particularly special about it. The only special things I have in it are an ICurrentUserService that I use for tracking the current scoped user. To create a DataProtectionKeyContext, the implementation is like so:

public class DataProtectionContext : BaseContext
{
    public override string DbSchema { get { return DatabaseSchema; } }
    public static string DatabaseSchema = "dbo";

    public DataProtectionContext(DbContextOptions<DataProtectionContext> options, ICurrentUserService currentUserService) :
    base(options, currentUserService)
    { }

    public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var assembly = typeof(DataProtectionContext).GetTypeInfo().Assembly;
        modelBuilder.AddAssemblyConfiguration<DataProtectionContext>(typeof(DataProtectionKeyConfiguration).Namespace);
    }
}

You can see this context is using the EntityTypeBuilder in the OnModelCreating method to load the fluent mappings for the DataProtectionKey object.

Now, we need a repository. I take advantage of the IRepository that I have defined and inherit the BaseRepository<T> But, its methods are really pertitent to the IXmlRepository. I implement the IXmlRepository on top of my own repository. My repository does expose the typed DbSet based on the type specification of the repository.

Getting the DbSet during the instantiation looks like this, btw:

_dbSet = _dbContext.Set<TEntity>();
public class DataProtectionKeyRepository : BaseRepository<DataProtectionKey>, IXmlRepository
{
    public DataProtectionKeyRepository(DataProtectionContext dbContext)
        : base(dbContext)
    { }

    public IReadOnlyCollection<XElement> GetAllElements()
    {
        return new ReadOnlyCollection<XElement>(_dbSet.Select(k => XElement.Parse(k.XmlData)).ToList());
    }

    public void StoreElement(XElement element, string friendlyName)
    {
        var entity = _dbSet.SingleOrDefault(k => k.FriendlyName == friendlyName);
        if (null != entity)
        {
            entity.XmlData = element.ToString();
            _dbSet.Update(entity);
        }
        else
        {
            _dbSet.Add(new DataProtectionKey
            {
                FriendlyName = friendlyName,
                XmlData = element.ToString()
            });
        }

        _dbContext.SaveChanges();
    }
} 

As you can see, the IXmlRepository methods are pretty simple. We only need storage and retrieval methods.

With the plumbing complete, we need to tell the DataProtection provider to use our stuff! If you were use file system storage, your startup might look like this (Startup.cs):

var encryptionSettings = new AuthenticatedEncryptionSettings()
{
    EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
    ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};

services
    .AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo($@"{_hostingEnv.ContentRootPath}\keys"))
    .SetDefaultKeyLifetime(TimeSpan.FromDays(7))
    .SetApplicationName("shared-app-name")
    .UseCryptographicAlgorithms(encryptionSettings);

I want an IDataProtectionBuilder extension, then, that will handle attaching the DbContext AND the IXmlRepository. Let’s call it PersistKeysToSql.

The signature of the extension method will take an optional connection string, schema, and an IServiceCollection. This things are needed if we want the extension method to add the DbContext. However, if they are not passed in, they would be handled outside of the extension.

public static class DataProtectionKeyExtensions
{
    public static IDataProtectionBuilder PersistKeysToSql(this IDataProtectionBuilder builder, string connectionString = "",
        string schema = "dbo", IServiceCollection services = null)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        // Add the context if desired
        if (!string.IsNullOrWhiteSpace(connectionString) && services != null)
        {
            services.AddDbContext<DataProtectionContext>(options =>
            {
                options.UseSqlServer(connectionString);
                options.UseLoggerFactory(null);
            }, ServiceLifetime.Scoped);
        }

        return builder.Use(ServiceDescriptor.Scoped<IXmlRepository, DataProtectionKeyRepository>());
    }

The “Use” method simply will ensure that we have only added one reference DataProtectionKeyRepository service descriptor.

    public static IDataProtectionBuilder Use(this IDataProtectionBuilder builder, ServiceDescriptor descriptor)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        if (descriptor == null)
        {
            throw new ArgumentNullException(nameof(descriptor));
        }

        for (int i = builder.Services.Count - 1; i >= 0; i--)
        {
            if (builder.Services[i]?.ServiceType == descriptor.ServiceType)
            {
                builder.Services.RemoveAt(i);
            }
        }

        builder.Services.Add(descriptor);

        return builder;
    }
}

Well, that’s it. Now, with my new configuration my Startup.cs, all of the DataProtection keys are stored in SQL server.

var encryptionSettings = new AuthenticatedEncryptionSettings()
{
    EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
    ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};

services
    .AddDataProtection()
    .PersistKeysToSql(Configuration.GetConnectionString("DataProtection"), "dbo", services)
    .SetDefaultKeyLifetime(TimeSpan.FromDays(7))
    .SetApplicationName("shared-app-name")
    .UseCryptographicAlgorithms(encryptionSettings);

On a side note, you can see that I have set the default key lifetime as seven days. One week is the minimum allowed it seems. I’ll also be porting this over to my older .NET 4.6.x OWIN applications that utilize the .NET Core DataProtector along with Entity Framework 6.x.

2 thoughts on “.NET Core SQL DataProtection Key Storage Provider using Entity Framework”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.