From b07232521b57658ef52afb467cf29243ce6f4471 Mon Sep 17 00:00:00 2001 From: nurdotnet <278048682+nurdotnet@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:42:53 +0500 Subject: [PATCH] fix(auth): return 401 instead of 302 for API challenges; persist dev signing key across restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the 404 on /api/admin/moysklad/test (and /api/me): - AddIdentity<> sets DefaultChallengeScheme = IdentityConstants.ApplicationScheme (cookies), so unauthorized API calls got 302 → /Account/Login → 404 instead of 401. - Ephemeral OpenIddict keys (AddEphemeralSigningKey) regenerated on every API restart, silently invalidating any JWT already stored in the browser. Fixes: - Explicitly set DefaultScheme / DefaultAuthenticateScheme / DefaultChallengeScheme to OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme so [Authorize] challenges now return 401 (axios interceptor can react + retry or redirect). - Replace ephemeral RSA keys with a persistent dev RSA key stored in src/food-market.api/App_Data/openiddict-dev-key.xml (gitignored). Generated on first run, reused on subsequent starts. Dev tokens now survive API restarts. Production must register proper X509 certificates via configuration. - .gitignore: add App_Data/, *.pem, openiddict-dev-key.xml patterns. - Web axios: on hard 401 with failed refresh, redirect to /login rather than leaving the user stuck on a protected screen. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 5 +++++ src/food-market.api/Program.cs | 27 +++++++++++++++++++++++++-- src/food-market.web/src/lib/api.ts | 4 ++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f165638..5f535d5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,10 +66,15 @@ pnpm-debug.log* ## Secrets *.pfx *.snk +*.pem secrets.json appsettings.Development.local.json appsettings.Production.local.json +## OpenIddict dev keys (local only, never commit) +src/food-market.api/App_Data/ +**/App_Data/openiddict-dev-key.xml + ## Docker / local .docker-data/ postgres-data/ diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs index 57058ca..453854b 100644 --- a/src/food-market.api/Program.cs +++ b/src/food-market.api/Program.cs @@ -66,9 +66,25 @@ opts.AcceptAnonymousClients(); opts.RegisterScopes(Scopes.OpenId, Scopes.Profile, Scopes.Email, Scopes.Roles, "api"); + // Persistent dev keys: RSA key stored in src/food-market.api/App_Data/openiddict-dev-key.xml. + // Survives API restarts so issued tokens remain valid across rebuilds. + // Never commit this file (it's in .gitignore via App_Data/). Production must use real certificates. + var keyPath = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "openiddict-dev-key.xml"); + var rsa = System.Security.Cryptography.RSA.Create(2048); + if (File.Exists(keyPath)) + { + rsa.FromXmlString(File.ReadAllText(keyPath)); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(keyPath)!); + File.WriteAllText(keyPath, rsa.ToXmlString(includePrivateParameters: true)); + } + var devKey = new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa) { KeyId = "food-market-dev" }; + opts.AddEncryptionKey(devKey); + opts.AddSigningKey(devKey); if (builder.Environment.IsDevelopment()) { - opts.AddEphemeralEncryptionKey().AddEphemeralSigningKey(); opts.DisableAccessTokenEncryption(); } @@ -87,7 +103,14 @@ opts.UseAspNetCore(); }); - builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + // Force OpenIddict validation to handle ALL [Authorize] challenges — otherwise AddIdentity's + // cookie scheme takes over and 401 becomes a 302 redirect to /Account/Login for API calls. + builder.Services.AddAuthentication(options => + { + options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme; + }); builder.Services.AddAuthorization(); builder.Services.AddControllers(); diff --git a/src/food-market.web/src/lib/api.ts b/src/food-market.web/src/lib/api.ts index f257589..332adf5 100644 --- a/src/food-market.web/src/lib/api.ts +++ b/src/food-market.web/src/lib/api.ts @@ -29,6 +29,10 @@ api.interceptors.response.use( return api(original) } clearTokens() + // Redirect to login so user isn't stuck on a protected page with stale tokens. + if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) { + window.location.href = '/login' + } } return Promise.reject(error) },