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) },