From 37099ea148d3a858b1e619938540313ac4ef216a Mon Sep 17 00:00:00 2001 From: CattonNyan Date: Mon, 7 Sep 2026 16:11:06 +0900 Subject: [PATCH 1/2] fix(cors): allow PATCH in restricted policy --- .../appsettings.Production.json | 2 +- src/Host/FSH.Starter.Api/appsettings.json | 2 +- .../Framework.Tests/Framework.Tests.csproj | 9 ++++++ .../Web/CorsConfigurationTests.cs | 29 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs diff --git a/src/Host/FSH.Starter.Api/appsettings.Production.json b/src/Host/FSH.Starter.Api/appsettings.Production.json index 332724534b..17e7924e40 100644 --- a/src/Host/FSH.Starter.Api/appsettings.Production.json +++ b/src/Host/FSH.Starter.Api/appsettings.Production.json @@ -60,7 +60,7 @@ "AllowAll": false, "AllowedOrigins": [], "AllowedHeaders": [ "content-type", "authorization" ], - "AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ] + "AllowedMethods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ] }, "JwtOptions": { "Issuer": "fsh.local", diff --git a/src/Host/FSH.Starter.Api/appsettings.json b/src/Host/FSH.Starter.Api/appsettings.json index 293fdfebb6..f21c4e52ce 100644 --- a/src/Host/FSH.Starter.Api/appsettings.json +++ b/src/Host/FSH.Starter.Api/appsettings.json @@ -101,7 +101,7 @@ "http://localhost:5174" ], "AllowedHeaders": [ "content-type", "authorization" ], - "AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ] + "AllowedMethods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ] }, "JwtOptions": { "Issuer": "fsh.local", diff --git a/src/Tests/Framework.Tests/Framework.Tests.csproj b/src/Tests/Framework.Tests/Framework.Tests.csproj index 89ec11ee87..8bc68c1bd3 100644 --- a/src/Tests/Framework.Tests/Framework.Tests.csproj +++ b/src/Tests/Framework.Tests/Framework.Tests.csproj @@ -32,4 +32,13 @@ + + + + + diff --git a/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs b/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs new file mode 100644 index 0000000000..a8b65f55ef --- /dev/null +++ b/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs @@ -0,0 +1,29 @@ +using System.Text.Json; + +namespace Framework.Tests.Web; + +public sealed class CorsConfigurationTests +{ + [Theory] + [InlineData("appsettings.json")] + [InlineData("appsettings.Production.json")] + public void AllowedMethods_Should_IncludePatch_When_RestrictedCorsIsConfigured(string fileName) + { + // Arrange + string path = Path.Combine(AppContext.BaseDirectory, "HostConfiguration", fileName); + + // Act + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path)); + JsonElement corsOptions = document.RootElement.GetProperty("CorsOptions"); + string[] allowedMethods = corsOptions + .GetProperty("AllowedMethods") + .EnumerateArray() + .Select(method => method.GetString()) + .OfType() + .ToArray(); + + // Assert + corsOptions.GetProperty("AllowAll").GetBoolean().ShouldBeFalse(); + allowedMethods.ShouldContain("PATCH"); + } +} From 2e5a650b6b198218ca66e398abf1a982f83f442d Mon Sep 17 00:00:00 2001 From: iammukeshm Date: Fri, 25 Sep 2026 07:13:52 +0530 Subject: [PATCH 2/2] fix(cors): allow the headers both clients send under the restricted policy The restricted policy only allowed content-type and authorization, so with CorsOptions:AllowAll=false the browser rejects the preflight for: - tenant (every API call from both apps) - X-FSH-App (login) - Idempotency-Key (chat sends) - X-Requested-With / X-SignalR-User-Agent (SignalR negotiate) Fixes #1367. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../appsettings.Production.json | 2 +- src/Host/FSH.Starter.Api/appsettings.json | 2 +- .../Web/CorsConfigurationTests.cs | 33 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Host/FSH.Starter.Api/appsettings.Production.json b/src/Host/FSH.Starter.Api/appsettings.Production.json index 17e7924e40..158870fa04 100644 --- a/src/Host/FSH.Starter.Api/appsettings.Production.json +++ b/src/Host/FSH.Starter.Api/appsettings.Production.json @@ -59,7 +59,7 @@ "CorsOptions": { "AllowAll": false, "AllowedOrigins": [], - "AllowedHeaders": [ "content-type", "authorization" ], + "AllowedHeaders": [ "content-type", "authorization", "tenant", "x-fsh-app", "idempotency-key", "x-requested-with", "x-signalr-user-agent" ], "AllowedMethods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ] }, "JwtOptions": { diff --git a/src/Host/FSH.Starter.Api/appsettings.json b/src/Host/FSH.Starter.Api/appsettings.json index f21c4e52ce..6964004664 100644 --- a/src/Host/FSH.Starter.Api/appsettings.json +++ b/src/Host/FSH.Starter.Api/appsettings.json @@ -100,7 +100,7 @@ "http://localhost:5173", "http://localhost:5174" ], - "AllowedHeaders": [ "content-type", "authorization" ], + "AllowedHeaders": [ "content-type", "authorization", "tenant", "x-fsh-app", "idempotency-key", "x-requested-with", "x-signalr-user-agent" ], "AllowedMethods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ] }, "JwtOptions": { diff --git a/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs b/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs index a8b65f55ef..930b28eb84 100644 --- a/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs +++ b/src/Tests/Framework.Tests/Web/CorsConfigurationTests.cs @@ -26,4 +26,37 @@ public void AllowedMethods_Should_IncludePatch_When_RestrictedCorsIsConfigured(s corsOptions.GetProperty("AllowAll").GetBoolean().ShouldBeFalse(); allowedMethods.ShouldContain("PATCH"); } + + // Every non-safelisted header the React clients (and the SignalR client) send must be allowed, + // or the browser rejects the preflight: tenant on every call, X-FSH-App on login, + // Idempotency-Key on chat sends, X-Requested-With / X-SignalR-User-Agent on hub negotiate. + [Theory] + [InlineData("appsettings.json", "tenant")] + [InlineData("appsettings.json", "x-fsh-app")] + [InlineData("appsettings.json", "idempotency-key")] + [InlineData("appsettings.json", "x-requested-with")] + [InlineData("appsettings.json", "x-signalr-user-agent")] + [InlineData("appsettings.Production.json", "tenant")] + [InlineData("appsettings.Production.json", "x-fsh-app")] + [InlineData("appsettings.Production.json", "idempotency-key")] + [InlineData("appsettings.Production.json", "x-requested-with")] + [InlineData("appsettings.Production.json", "x-signalr-user-agent")] + public void AllowedHeaders_Should_IncludeClientHeader_When_RestrictedCorsIsConfigured(string fileName, string header) + { + // Arrange + string path = Path.Combine(AppContext.BaseDirectory, "HostConfiguration", fileName); + + // Act + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path)); + string[] allowedHeaders = document.RootElement + .GetProperty("CorsOptions") + .GetProperty("AllowedHeaders") + .EnumerateArray() + .Select(h => h.GetString()) + .OfType() + .ToArray(); + + // Assert + allowedHeaders.ShouldContain(h => string.Equals(h, header, StringComparison.OrdinalIgnoreCase)); + } }