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.Dashboardregistered inDashboard/DashboardServiceCollectionExtensions.cs::AddGatewayDashboard(AddAuthentication("MxGateway.Dashboard").AddCookie(...)), plus a bearer schemeMxGateway.Dashboard.HubToken(HubTokenAuthenticationHandler) for SignalR hubs. - Real login:
/login→DashboardAuthenticator.AuthenticateAsync→ sharedILdapAuthServicebind/search →IGroupRoleMapper<string>→CreatePrincipalbuilds aClaimsPrincipal(ZbClaimTypes.Name/Username/DisplayName+ oneZbClaimTypes.Roleper role +LdapGroupClaimTypegroup claims; identity authType = the cookie scheme, nameType =ZbClaimTypes.Name, roleType =ZbClaimTypes.Role) → cookie sign-in. - Authorization: a custom
DashboardAuthorizationHandlerevaluatesDashboardAuthorizationRequirement. Policies:ViewerPolicy(AnyDashboardRole),AdminPolicy(AdminOnly),HubClientsPolicy(cookie or hub-token scheme, AnyDashboardRole). - Roles: exactly two —
DashboardRoles.Admin("Administrator") andDashboardRoles.Viewer("Viewer"). - Existing escapes (important):
DashboardAuthorizationHandleralready short-circuits whenAuthentication.Mode == Disabledor whenDashboard.AllowAnonymousLocalhost(default true) and the request is loopback. But both onlycontext.Succeed(...)the authorization requirement — they do not mint an authenticated principal. SoHttpContext.User.Identity.IsAuthenticatedstays false,Identity.Nameis null, and role-gatedAuthorizeViewwrite 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 asName/Username/DisplayNameof 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 ofAddCookie; theHubTokenscheme 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 optionsPostConfigure<ILoggerFactory>idiom OtOpcUa uses. - The existing
AllowAnonymousLocalhost/Authentication.Mode == Disabledescapes are left untouched —DisableLoginis 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). /loginstill renders when the flag is on but is pointless (the user is already authenticated);POST /login'sSignInAsyncis a no-op./logoutis 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:
HandleAuthenticateAsyncreturnsSuccess;principal.Identity.IsAuthenticated,Identity.Name == AutoLoginUser,IsInRole("Administrator")&&IsInRole("Viewer"); blank-user fallback. - Wiring / integration (
WebApplicationFactory): withDisableLogin=true, anAdminPolicy-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— newMxGateway:Dashboard:DisableLogin/AutoLoginUseroptions.- The dashboard design doc (
docs/GatewayDashboardDesign.md). - The CLAUDE.md dashboard-auth note (alongside the
AllowAnonymousLocalhostmention).
Scope / verification
Gateway-server-side only (.NET 10, x64) — builds and tests entirely on macOS. No worker,
no .proto, no client, no gRPC changes.