Files
mxaccessgw/docs/plans/2026-06-16-dashboard-disable-login-design.md
T

7.8 KiB

Dashboard "Disable Login" Dev Flag — Design

Date: 2026-06-16 Status: Approved (brainstorming) — ready for implementation plan.

Goal

A config flag that disables login in the gateway dashboard. When enabled, every request is auto-authenticated as a fixed dev user (default multi-role) holding both dashboard roles (Administrator + Viewer), so no login form, cookie, or LDAP bind is involved and the whole UI behaves as a signed-in multi-role admin. Default off. Mirrors the sister project OtOpcUa's Security:Auth:DisableLogin feature.

Why / scope

Speeds up dashboard testing against the remote dev boxes (10.100.0.48, wonder) with no sign-in round-trip and no GLAuth dependency. Scope is the dashboard cookie web surface only — the gRPC API-key auth path (authorization: Bearer mxgw_…) and its scopes are a separate auth model and are untouched.

Background (current dashboard auth, verified)

  • Dashboard auth is a single cookie scheme MxGateway.Dashboard registered in Dashboard/DashboardServiceCollectionExtensions.cs::AddGatewayDashboard (AddAuthentication("MxGateway.Dashboard").AddCookie(...)), plus a bearer scheme MxGateway.Dashboard.HubToken (HubTokenAuthenticationHandler) for SignalR hubs.
  • Real login: /loginDashboardAuthenticator.AuthenticateAsync → shared ILdapAuthService bind/search → IGroupRoleMapper<string>CreatePrincipal builds a ClaimsPrincipal (ZbClaimTypes.Name/Username/DisplayName + one ZbClaimTypes.Role per role + LdapGroupClaimType group claims; identity authType = the cookie scheme, nameType = ZbClaimTypes.Name, roleType = ZbClaimTypes.Role) → cookie sign-in.
  • Authorization: a custom DashboardAuthorizationHandler evaluates DashboardAuthorizationRequirement. Policies: ViewerPolicy (AnyDashboardRole), AdminPolicy (AdminOnly), HubClientsPolicy (cookie or hub-token scheme, AnyDashboardRole).
  • Roles: exactly two — DashboardRoles.Admin ("Administrator") and DashboardRoles.Viewer ("Viewer").
  • Existing escapes (important): DashboardAuthorizationHandler already short-circuits when Authentication.Mode == Disabled or when Dashboard.AllowAnonymousLocalhost (default true) and the request is loopback. But both only context.Succeed(...) the authorization requirement — they do not mint an authenticated principal. So HttpContext.User.Identity.IsAuthenticated stays false, Identity.Name is null, and role-gated AuthorizeView write affordances stay hidden. That is precisely why they do not deliver the "logged-in multi-role admin" experience this feature needs.

When the flag is on, replace the .AddCookie(...) registration with a custom AuthenticationHandler registered under the same scheme name (DashboardAuthenticationDefaults.AuthenticationScheme = "MxGateway.Dashboard"). Its HandleAuthenticateAsync always returns AuthenticateResult.Success with the fixed dev principal (configured username, both roles), shaped identically to what DashboardAuthenticator.CreatePrincipal produces. UseAuthentication() stamps that principal on HttpContext.User for every request.

Registering under the cookie scheme name (not a new name) is the load-bearing detail: the ViewerPolicy, AdminPolicy, and HubClientsPolicy all resolve through that scheme via DashboardAuthorizationHandler's role check, so they pass with no policy or page changes. The HTTP pipeline (Razor pages, admin endpoints), the Blazor circuit (AuthorizeView, [CascadingParameter] AuthenticationState), and the SignalR hubs are all covered by the single HttpContext.User seam. Because the handler authenticates every request, the feature is inherently global (all clients, including remote browsers) — the agreed scope.

SignInAsync/SignOutAsync are no-ops (no cookie to write or clear; the next request re-authenticates through the handler).

Alternatives rejected: (2) mint the principal inside the existing DashboardAuthorizationHandler bypass branches — authorization runs after authentication, so HttpContext.User is set too late for the Blazor auth state, and two seams must agree (this is essentially today's half-feature); (3) a pipeline middleware plus a stubbed AuthenticationStateProvider — two components to keep in sync, and a page request still 302s to /login unless HttpContext.User is also set.

Components

1. Config surface — two new fields on DashboardOptions (MxGateway:Dashboard:*)

  • DisableLogin (bool, default false).
  • AutoLoginUser (string, default "multi-role" — this project's GLAuth Administrator test user). Used as Name/Username/DisplayName of the minted principal; blank falls back to "multi-role".

"All permissions" = principal minted with both DashboardRoles.Admin and DashboardRoles.Viewer.

2. DashboardAutoLoginAuthenticationHandler

AuthenticationHandler<AuthenticationSchemeOptions> implementing IAuthenticationSignInHandler. Mirrors OtOpcUa's AutoLoginAuthenticationHandler, adapted to this project's claim shape (ZbClaimTypes.*, DashboardRoles.*). Always Success; SignIn/SignOut no-ops.

3. Wiring in AddGatewayDashboard

Read MxGateway:Dashboard:DisableLogin directly from IConfiguration at registration time (the same idiom OtOpcUa uses, since scheme registration precedes options binding).

  • On → AddScheme<AuthenticationSchemeOptions, DashboardAutoLoginAuthenticationHandler>( "MxGateway.Dashboard", _ => {}) in place of AddCookie; the HubToken scheme stays registered unchanged.
  • Off → existing AddCookie(...) path unchanged.

4. Safety

  • Default off.
  • A loud one-time startup LogWarning ("DASHBOARD LOGIN DISABLED (MxGateway:Dashboard:DisableLogin=true) — every request authenticated as '{user}' with full permissions (Administrator, Viewer). Dev/test only; never enable in production.") via the same options PostConfigure<ILoggerFactory> idiom OtOpcUa uses.
  • The existing AllowAnonymousLocalhost / Authentication.Mode == Disabled escapes are left untouched — DisableLogin is orthogonal (it changes authentication, minting a principal, not authorization bypass); when it is on the authorization handler's normal role-check branch succeeds, so the bypass branches simply do not matter.

Error handling / edge cases

  • Blank AutoLoginUser → falls back to "multi-role" (handler never mints a nameless principal).
  • /login still renders when the flag is on but is pointless (the user is already authenticated); POST /login's SignInAsync is a no-op. /logout is likewise a no-op. No redirect added (YAGNI).
  • No interaction with the gRPC API-key path — that auth is entirely separate.

Testing

  • Handler unit test: HandleAuthenticateAsync returns Success; principal.Identity.IsAuthenticated, Identity.Name == AutoLoginUser, IsInRole("Administrator") && IsInRole("Viewer"); blank-user fallback.
  • Wiring / integration (WebApplicationFactory): with DisableLogin=true, an AdminPolicy-gated endpoint returns 200 with no cookie, and a /hubs/* negotiate authorizes; the startup warning is emitted.
  • Regression: with the flag off (default), the real cookie handler is still registered and existing dashboard auth tests pass.

Docs to update in the same change

  • docs/GatewayConfiguration.md — new MxGateway:Dashboard:DisableLogin / AutoLoginUser options.
  • The dashboard design doc (docs/GatewayDashboardDesign.md).
  • The CLAUDE.md dashboard-auth note (alongside the AllowAnonymousLocalhost mention).

Scope / verification

Gateway-server-side only (.NET 10, x64) — builds and tests entirely on macOS. No worker, no .proto, no client, no gRPC changes.