[Infra] Persist DataProtection keys in the DB (fixes logout/antiforgery on deploy)

Add Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; AppDbContext implements IDataProtectionKeyContext with a DataProtectionKeys set; PersistKeysToDbContext + SetApplicationName(hamkadr). Now the key ring is shared across restarts/replicas, so auth cookies, antiforgery tokens and the captcha no longer break on every deploy (the root cause of the earlier admin lock-out). Migration: DataProtectionKeys table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 07:33:20 +03:30
parent c46e628f6a
commit 437258294b
6 changed files with 1336 additions and 1 deletions
+5 -1
View File
@@ -1,12 +1,16 @@
using JobsMedical.Web.Models;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace JobsMedical.Web.Data;
public class AppDbContext : DbContext
public class AppDbContext : DbContext, IDataProtectionKeyContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
/// <summary>DataProtection key ring — persisted so antiforgery/cookies survive deploys & replicas.</summary>
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
public DbSet<City> Cities => Set<City>();
public DbSet<District> Districts => Set<District>();
public DbSet<Role> Roles => Set<Role>();
@@ -11,6 +11,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="WebPush" Version="1.0.12" />
</ItemGroup>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace JobsMedical.Web.Migrations
{
/// <inheritdoc />
public partial class DataProtectionKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DataProtectionKeys",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
FriendlyName = table.Column<string>(type: "text", nullable: true),
Xml = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DataProtectionKeys", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DataProtectionKeys");
}
}
}
@@ -970,6 +970,25 @@ namespace JobsMedical.Web.Migrations
b.ToTable("WebPushSubscriptions");
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("FriendlyName")
.HasColumnType("text");
b.Property<string>("Xml")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
modelBuilder.Entity("JobsMedical.Web.Models.Application", b =>
{
b.HasOne("JobsMedical.Web.Models.User", "Doctor")
+8
View File
@@ -3,6 +3,7 @@ using System.Text.Unicode;
using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using JobsMedical.Web.Services;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
@@ -77,6 +78,13 @@ builder.Services.AddSingleton(HtmlEncoder.Create(
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
// Persist the DataProtection key ring in the DB so antiforgery tokens, auth cookies and the
// captcha survive deploys/restarts (otherwise a new key ring each boot logs everyone out and
// breaks antiforgery — the cause of the earlier admin lock-out).
builder.Services.AddDataProtection()
.PersistKeysToDbContext<AppDbContext>()
.SetApplicationName("hamkadr");
var app = builder.Build();
// Apply migrations + seed on startup (fine for MVP single-instance deploy).