Using TeamCity, Bitbucket and msdeploy, I have been able to develop a satisfactory CI process for my web apps. However, I found that whenever an application was deployed to production, the user sessions would expire. Initially, it was not a big issue, but as the number of users grew so did the issue. We want to make the deployment process as smooth and as painless for our uses as possible.
IIS persists user sessions in the application memory. Whenever the application pool is restarted, all the memory was wiped clean and the information was lost. It took me a while to research the solution, but I eventually tracked the solution down to services.AddDataProtection()
. I strongly recommend reading Microsoft's Configure ASP.NET Core Data Protection as it offers a valuable insight into potential security breaches which might occur as a result of applying the changes in this article.
Using file system storage
Out of the box, Microsoft offers to store keys in PersistKeysToAzureBlobStorage
, PersistKeysToFileSystem
and PersistKeysToRegistry
.
1 2 3 4 5 |
services.AddDataProtection() // This helps surviving a restart: a same app will find back its keys. Just ensure to create the folder. .PersistKeysToFileSystem(new DirectoryInfo("PATH TO YOUR DIRECTORY WITH KEYS")) // This helps surviving a site update: each app has its own store, building the site creates a new app .SetApplicationName("YOUR APP NAME") |
File system would have been sufficient for what I needed. Unfortunately my hosting provider restricted access to the file system; hence preventing my from applying this approach.
Using SQL Server
EF code-first model class
1 2 3 4 5 6 7 8 |
public class DataProtectionKey { [Key] public string KeyId { get; set; } [Column(TypeName = "nvarchar(max)")] public string XmlData { get; set; } } |
DbContext
and run Add-Migration
and Update-database
.
Repository for the model
Add the repository. Note the inheritance from IXmlRepository
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public interface IDataProtectionKeyRepository : IXmlRepository { } public class DataProtectionKeyRepository : IDataProtectionKeyRepository { private readonly IdentityContext _context; public DataProtectionKeyRepository(IdentityContext context) { this._context = context; } public IReadOnlyCollection GetAllElements() { return new ReadOnlyCollection(_context.DataProtectionKeys.Select(k => XElement.Parse(k.XmlData)).ToList()); } public void StoreElement(XElement element, string keyId) { var entity = _context.DataProtectionKeys.SingleOrDefault(k => k.KeyId == keyId); if (null != entity) { entity.XmlData = element.ToString(); _context.DataProtectionKeys.Update(entity); } else { _context.DataProtectionKeys.Add(new DataProtectionKey { KeyId = KeyId, XmlData = element.ToString() }); } _context.SaveChanges(); } } |
Just remember to point to your own DbContext. You may wish to create a separate context (either db or schema) to handle these. .NET Core SQL DataProtection Key Storage Provider using Entity Framework by Longing to Know covers this in more detail.
Configure Startup
1 |
services.AddScoped<IDataProtectionKeyRepository, DataProtectionKeyRepository>(); |
1 2 3 4 5 6 |
services.AddDataProtection() // This helps surviving a restart: a same app will find back its keys. Just ensure to create the folder. .AddKeyManagementOptions(options => options.XmlRepository = services.BuildServiceProvider().GetService<IDataProtectionKeyRepository>()) // This helps surviving a site update: each app has its own store, building the site creates a new app .SetApplicationName("YOUR APP NAME") .SetDefaultKeyLifetime(TimeSpan.FromDays(7)); |