diff --git a/Marechai/Services/TokenProvider.cs b/Marechai/Services/TokenProvider.cs index 4ec65a0d..0cd3d33b 100644 --- a/Marechai/Services/TokenProvider.cs +++ b/Marechai/Services/TokenProvider.cs @@ -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; } diff --git a/Marechai/Startup.cs b/Marechai/Startup.cs index 5b3505eb..02ebb888 100644 --- a/Marechai/Startup.cs +++ b/Marechai/Startup.cs @@ -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";