diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 0bfb769..07c1c04 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -148,6 +148,7 @@ the affected stream while the MXAccess session remains active. | `MxGateway:Dashboard:Enabled` | `true` | Enables Blazor Server dashboard route mapping. The dashboard mounts at the host root (`/`); there is no separate path-base prefix. | | `MxGateway:Dashboard:AllowAnonymousLocalhost` | `true` | Allows loopback dashboard requests to bypass the dashboard cookie requirement for local development. Remote requests still require dashboard authentication. | | `MxGateway:Dashboard:RequireHttpsCookie` | `true` | Sets the dashboard auth cookie's secure policy. `true` keeps `CookieSecurePolicy.Always` — the cookie is only sent over HTTPS, which matches a production HTTPS deployment. Set to `false` for plain-HTTP dev deployments to use `CookieSecurePolicy.SameAsRequest`; the cookie is still flagged Secure on HTTPS requests, but it can round-trip over HTTP. Browsers drop Secure cookies set over HTTP from non-localhost hosts, so leaving this `true` while serving the dashboard over plain HTTP will break login from any remote browser. | +| `MxGateway:Dashboard:CookieName` | `MxGatewayDashboard` | Dashboard auth cookie name. Leave unset (null/blank) to use the default. Override it to give a distinct name to a gateway that shares a hostname with another gateway instance: browser cookies are scoped by host+path but **not** by port, so two instances on the same host would otherwise clobber each other's dashboard session under a shared cookie name. Changing it signs out existing dashboard sessions on next deploy. | | `MxGateway:Dashboard:SnapshotIntervalMilliseconds` | `1000` | Dashboard snapshot refresh interval used by the snapshot SignalR hub and the pages that subscribe to it. | | `MxGateway:Dashboard:RecentFaultLimit` | `100` | Maximum number of fault summaries projected into each dashboard snapshot. | | `MxGateway:Dashboard:RecentSessionLimit` | `200` | Maximum number of session summaries projected into each dashboard snapshot. | diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs index 7a56ef6..3b38ea1 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs @@ -21,6 +21,17 @@ public sealed class DashboardOptions /// public bool RequireHttpsCookie { get; init; } = true; + /// + /// Dashboard auth cookie name. When null/blank (the default) the canonical + /// + /// is used. Override it (MxGateway:Dashboard:CookieName) to give a distinct name to a + /// gateway that shares a hostname with another gateway instance — browser cookies are scoped + /// by host+path but NOT by port, so two instances on the same host would otherwise clobber + /// each other's dashboard session under a shared cookie name. Changing this signs out + /// existing dashboard sessions on next deploy. + /// + public string? CookieName { get; init; } + /// Gets the dashboard snapshot update interval in milliseconds. public int SnapshotIntervalMilliseconds { get; init; } = 1_000; diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs index 806fe41..b87708b 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs @@ -66,6 +66,8 @@ public static class DashboardServiceCollectionExtensions ZbCookieDefaults.Apply(cookieOptions, requireHttps: true, idleTimeout: TimeSpan.FromHours(8)); // Cookie name, path, and redirect paths are MxGateway-specific — set after Apply // so they are never overwritten by the shared helper (Apply intentionally skips name). + // This is the canonical default; it is overridden per-environment from + // DashboardOptions.CookieName by the PostConfigure below. cookieOptions.Cookie.Name = DashboardAuthenticationDefaults.CookieName; cookieOptions.Cookie.Path = "/"; cookieOptions.LoginPath = "/login"; @@ -77,13 +79,22 @@ public static class DashboardServiceCollectionExtensions _ => { }); // Honour DashboardOptions.RequireHttpsCookie (default true / Always; set false for dev - // HTTP deployments → SameAsRequest). This overrides the Apply default above. + // HTTP deployments → SameAsRequest) and the optional per-environment cookie-name + // override. Both run after the inline AddCookie config above, so they win. services.AddOptions(DashboardAuthenticationDefaults.AuthenticationScheme) .Configure>((cookieOptions, gatewayOptions) => { cookieOptions.Cookie.SecurePolicy = gatewayOptions.Value.Dashboard.RequireHttpsCookie ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest; + + // Config-driven cookie name (MxGateway:Dashboard:CookieName). Null/blank keeps + // the canonical default set above, so a misconfiguration cannot unname the cookie. + var cookieName = gatewayOptions.Value.Dashboard.CookieName; + if (!string.IsNullOrWhiteSpace(cookieName)) + { + cookieOptions.Cookie.Name = cookieName; + } }); services.AddAuthorization(authorization => diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs index 95ed26c..40fcfea 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs @@ -49,4 +49,23 @@ public sealed class DashboardCookieOptionsTests Assert.Equal(CookieSecurePolicy.SameAsRequest, options.Cookie.SecurePolicy); } + + /// + /// Verifies that MxGateway:Dashboard:CookieName overrides the dashboard auth + /// cookie name, so a gateway instance sharing a hostname with another can be given a + /// distinct name (browser cookies are scoped by host+path, not port). + /// + [Fact] + public async Task Build_WithCookieNameOverride_UsesConfiguredName() + { + await using WebApplication app = GatewayApplication.Build( + ["--MxGateway:Dashboard:CookieName=MxGatewayDashboard.env2"]); + IOptionsMonitor optionsMonitor = app.Services + .GetRequiredService>(); + + CookieAuthenticationOptions options = optionsMonitor.Get( + DashboardAuthenticationDefaults.AuthenticationScheme); + + Assert.Equal("MxGatewayDashboard.env2", options.Cookie.Name); + } }