Implement data protection key persistence to ensure token stability across process restarts

This commit is contained in:
2026-05-09 20:27:39 +01:00
parent 59221b71a3
commit 379ac1d620
2 changed files with 36 additions and 0 deletions

View File

@@ -24,6 +24,7 @@
*******************************************************************************/
using System;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.JSInterop;
@@ -57,6 +58,24 @@ public sealed class TokenProvider(ProtectedLocalStorage localStorage)
// Circuit not ready or JS error. Leave uninitialized so the next call retries.
_cachedToken = null;
}
catch(CryptographicException)
{
// The data protection key that encrypted the cached token is no longer in the key
// ring (e.g. the key directory was wiped or the app's key ring rotated past it).
// Treat the stale ciphertext as "no token" and best-effort delete it so the user
// simply ends up logged out instead of seeing a server error every render.
_cachedToken = null;
_initialized = true;
try
{
await localStorage.DeleteAsync(StorageKey);
}
catch
{
// Best-effort cleanup; ignore JS / prerender failures here.
}
}
return _cachedToken;
}

View File

@@ -25,6 +25,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using MudBlazor.Services;
@@ -33,6 +34,7 @@ using Marechai.Services;
using Marechai.Shared;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
@@ -64,6 +66,21 @@ public class Startup(IConfiguration configuration)
{
services.AddMudServices();
// Persist the ASP.NET Core data protection keys to a stable on-disk directory so the
// tokens we encrypt into ProtectedLocalStorage survive process restarts. Without this
// every restart generates a fresh ephemeral key, which makes the next request from a
// returning user crash with "The key {guid} was not found in the key ring" when the
// auth state provider tries to unprotect the cached JWT. SetApplicationName pins the
// ring to this app so multiple Marechai-family apps cannot accidentally share keys.
string keyRingPath = Configuration["DataProtection:KeyRingPath"]
?? Path.Combine(AppContext.BaseDirectory, "keys");
Directory.CreateDirectory(keyRingPath);
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keyRingPath))
.SetApplicationName("Marechai.Blazor");
services.AddMarechaiEmail(Configuration);
string apiUrl = Configuration.GetSection("ApiClient:Url").Value ?? "http://localhost:5023";