# 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: `/login` → `DashboardAuthenticator.AuthenticateAsync` → shared `ILdapAuthService` bind/search → `IGroupRoleMapper` → `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. ## Approach (chosen: always-authenticating handler under the cookie scheme name) 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` 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( "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` 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.