[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:
@@ -1,12 +1,16 @@
|
|||||||
using JobsMedical.Web.Models;
|
using JobsMedical.Web.Models;
|
||||||
|
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace JobsMedical.Web.Data;
|
namespace JobsMedical.Web.Data;
|
||||||
|
|
||||||
public class AppDbContext : DbContext
|
public class AppDbContext : DbContext, IDataProtectionKeyContext
|
||||||
{
|
{
|
||||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
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<City> Cities => Set<City>();
|
||||||
public DbSet<District> Districts => Set<District>();
|
public DbSet<District> Districts => Set<District>();
|
||||||
public DbSet<Role> Roles => Set<Role>();
|
public DbSet<Role> Roles => Set<Role>();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
<PackageReference Include="WebPush" Version="1.0.12" />
|
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+1267
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");
|
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 =>
|
modelBuilder.Entity("JobsMedical.Web.Models.Application", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("JobsMedical.Web.Models.User", "Doctor")
|
b.HasOne("JobsMedical.Web.Models.User", "Doctor")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.Unicode;
|
|||||||
using JobsMedical.Web.Data;
|
using JobsMedical.Web.Data;
|
||||||
using JobsMedical.Web.Models;
|
using JobsMedical.Web.Models;
|
||||||
using JobsMedical.Web.Services;
|
using JobsMedical.Web.Services;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -77,6 +78,13 @@ builder.Services.AddSingleton(HtmlEncoder.Create(
|
|||||||
builder.Services.AddDbContext<AppDbContext>(opt =>
|
builder.Services.AddDbContext<AppDbContext>(opt =>
|
||||||
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Apply migrations + seed on startup (fine for MVP single-instance deploy).
|
// Apply migrations + seed on startup (fine for MVP single-instance deploy).
|
||||||
|
|||||||
Reference in New Issue
Block a user