fix(dashboard): make dashboard auth cookie name configurable
The dashboard auth cookie name was hardcoded to the constant DashboardAuthenticationDefaults.CookieName (MxGatewayDashboard). Browser cookies are scoped by host+path but NOT by port, so two gateway instances sharing a hostname would clobber each other's dashboard session under the shared name. Add DashboardOptions.CookieName (MxGateway:Dashboard:CookieName); null/blank keeps the canonical default. Applied in the existing dashboard cookie PostConfigure (runs after the inline AddCookie default, so it wins). Behaviour is unchanged when unset. Adds a Tests case for the override.
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -21,6 +21,17 @@ public sealed class DashboardOptions
|
||||
/// </summary>
|
||||
public bool RequireHttpsCookie { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Dashboard auth cookie name. When null/blank (the default) the canonical
|
||||
/// <see cref="ZB.MOM.WW.MxGateway.Server.Dashboard.DashboardAuthenticationDefaults.CookieName"/>
|
||||
/// is used. Override it (<c>MxGateway:Dashboard:CookieName</c>) 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.
|
||||
/// </summary>
|
||||
public string? CookieName { get; init; }
|
||||
|
||||
/// <summary>Gets the dashboard snapshot update interval in milliseconds.</summary>
|
||||
public int SnapshotIntervalMilliseconds { get; init; } = 1_000;
|
||||
|
||||
|
||||
@@ -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<CookieAuthenticationOptions>(DashboardAuthenticationDefaults.AuthenticationScheme)
|
||||
.Configure<IOptions<GatewayOptions>>((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 =>
|
||||
|
||||
@@ -49,4 +49,23 @@ public sealed class DashboardCookieOptionsTests
|
||||
|
||||
Assert.Equal(CookieSecurePolicy.SameAsRequest, options.Cookie.SecurePolicy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <c>MxGateway:Dashboard:CookieName</c> 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).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Build_WithCookieNameOverride_UsesConfiguredName()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build(
|
||||
["--MxGateway:Dashboard:CookieName=MxGatewayDashboard.env2"]);
|
||||
IOptionsMonitor<CookieAuthenticationOptions> optionsMonitor = app.Services
|
||||
.GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>();
|
||||
|
||||
CookieAuthenticationOptions options = optionsMonitor.Get(
|
||||
DashboardAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
Assert.Equal("MxGatewayDashboard.env2", options.Cookie.Name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user