# Disable-Login (Dev Auto-Login) Flag — Design **Date:** 2026-06-16 **Status:** Approved (design); implementation plan to follow. **Branch:** `feature/disable-login` (off `main`, which now contains the merged M2 work). ## Goal Port the sister project **OtOpcUa**'s "disable login" config flag to ScadaBridge: a dev/test flag that, when enabled, bypasses the login form entirely and auto-authenticates **every request** as the `multi-role` user with **all** ScadaBridge roles (full permissions, system-wide / all sites). Default OFF. ## Scope decisions (confirmed with user) - **Safety guarding:** *Faithful copy* of OtOpcUa — **no environment guard**. The flag works in any `ASPNETCORE_ENVIRONMENT`; the only protection is a loud `LogWarning` on startup. (Noted risk: unlike OtOpcUa's AdminUI, this disables authentication on a SCADA **control** surface — the warning wording is emphatic and the doc-comment says "never enable in production." No code guard, by explicit choice.) - **Workflow:** the completed M2 (Tier-2) work was merged to `main` first (`b2d8fd8`); this feature is built on `feature/disable-login` off that updated main, so it can reuse M2.19's `SessionClaimBuilder`. - **Mechanism:** a custom ASP.NET Core `AuthenticationHandler` registered **under the cookie scheme name** when the flag is set (OtOpcUa's exact mechanism) — not request-middleware, not a Blazor `AuthenticationStateProvider` override. ## Reference: how OtOpcUa does it (Verified read-only against `/Users/dohertj2/Desktop/OtOpcUa`.) - Config key `Security:Auth:DisableLogin` (bool) + `Security:Auth:User` (string, default `"multi-role-test"`), bound to `AuthDisableLoginOptions` (`SectionName = "Security:Auth"`). - `AutoLoginAuthenticationHandler` — `HandleAuthenticateAsync` mints a `ClaimsPrincipal` with `Name`/`Username`/`DisplayName` + one `Role` claim per `DevAuthRoles.All` (all enum role names + `"Operator"`), returns `AuthenticateResult.Success`. **No cookie written** — every request re-authenticates fresh. `SignInAsync`/`SignOutAsync` are explicit **no-ops** (required, because `/auth/logout` calls `HttpContext.SignOutAsync()` on the scheme; without the no-ops the framework throws casting the handler to a sign-out handler). - Wiring (startup): reads the flag; if true, `AddAuthentication(Cookies)` then `.AddScheme(Cookies, _ => {})` **instead of** `.AddCookie(...)`. Since every authorization policy names the cookie scheme, they all authenticate through the handler — **zero policy changes**. - Safety: **no environment guard**; a `PostConfigure` emits one loud `LogWarning` naming the user + roles. The login page is not hidden — it is simply never reached because every request is already authenticated. ## ScadaBridge adaptation ScadaBridge has its own role vocabulary and (post-M2.19) a canonical claim builder; the port adapts to both. ### 1. Config / options New `AuthDisableLoginOptions` in `ZB.MOM.WW.ScadaBridge.Security`: ```csharp public sealed class AuthDisableLoginOptions { public const string SectionName = "ScadaBridge:Security:Auth"; public bool DisableLogin { get; set; } // default false public string User { get; set; } = "multi-role"; // canonical ScadaBridge test user } ``` `appsettings.json` ships with the section absent or `"DisableLogin": false`. The flag is only set `true` in local/dev configs (e.g. a docker-dev env var `ScadaBridge__Security__Auth__DisableLogin=true`). ### 2. Roles — single source of truth ScadaBridge declares exactly four roles in `Roles.cs`: `Administrator`, `Designer`, `Deployer`, `Viewer`. Add a `Roles.All` array (mirrors OtOpcUa's `DevAuthRoles.All`) so "all permissions" stays in sync if a role is added: ```csharp public static readonly string[] All = [Administrator, Designer, Deployer, Viewer]; ``` ### 3. Auto-login handler `AutoLoginAuthenticationHandler` in `ZB.MOM.WW.ScadaBridge.Security`, deriving from `SignInAuthenticationHandler` (so it satisfies the sign-in/sign-out handler contract). It reuses **M2.19's `SessionClaimBuilder.Build(...)`** for claim parity with a real login: ```csharp var user = string.IsNullOrWhiteSpace(_opts.User) ? "multi-role" : _opts.User; var mapping = new RoleMappingResult(Roles.All, [], IsSystemWideDeployment: true); var principal = SessionClaimBuilder.Build( username: user, displayName: user, groups: [], mapping: mapping, refreshTimestamp: now); return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)); ``` This yields: `Name`/`Username`/`DisplayName` = the user; all four `Role` claims; **no `ScopeId` claims** (`IsSystemWideDeployment: true` ⇒ system-wide, all sites). The `LastActivity`/`LastRoleRefresh` stamps that `SessionClaimBuilder` adds are harmless here (the auto-login scheme has no `OnValidatePrincipal`, so M2.19's idle/refresh logic is simply bypassed — correct for a dev bypass). `now` comes from the injected `TimeProvider`. `SignInAsync` and `SignOutAsync` are **no-ops** (so `/auth/logout` does not throw). `_opts` is the resolved `AuthDisableLoginOptions`. ### 4. Wiring The scheme choice is build-time and config-coupled, mirroring how `AddZbLdapAuth(configuration, …)` is called at the Host root immediately before `AddSecurity()`. Thread the flag into `AddSecurity` (re-add a minimal parameter — the shape it had before the LDAP cutover, e.g. `AddSecurity(this IServiceCollection services, bool disableLogin = false)`): - **flag false (default):** unchanged — `AddAuthentication(Cookies).AddCookie(...)` with the existing cookie hardening + M2.19 `OnValidatePrincipal`. - **flag true:** `AddAuthentication(Cookies).AddScheme(Cookies, _ => {})` instead of `AddCookie`, and emit the loud startup `LogWarning`. `Program.cs` (Host) reads `ScadaBridge:Security:Auth:DisableLogin` from `builder.Configuration` and passes it to `AddSecurity(...)`. `AddOptions().Bind(section)` is registered so the handler can resolve `User`. ### 5. Safety (faithful copy — warning only) No environment guard. One emphatic `LogWarning` at startup when the flag is on, e.g.: > "AUTH DISABLED (ScadaBridge:Security:Auth:DisableLogin=true) — every request is authenticated as > '{User}' with FULL permissions ({Roles}) across ALL sites. This is a SCADA control surface; > dev/test ONLY — never enable in production." The `AuthDisableLoginOptions` doc-comment repeats "Default OFF. Never enable in production." ### 6. Unchanged Login page, `/auth/login`, `RoleMapper`, LDAP (`AddZbLdapAuth`), cookie hardening, and the M2.19 cookie session validator are all untouched. In disable-login mode the login-redirect path is never reached because every request is pre-authenticated. ## Error handling - `HandleAuthenticateAsync` always returns success — it cannot fail (no I/O, no LDAP, no DB). - No-op sign-in/out cannot throw. - If `User` is blank, fall back to `"multi-role"`. ## Testing xUnit, in `ZB.MOM.WW.ScadaBridge.Security.Tests` (mirroring the existing harness): 1. **Handler** — `HandleAuthenticateAsync` returns an authenticated principal whose identity name is the configured user, carries all four `Roles.All` role claims, and has **no `ScopeId`** claim (system-wide). A blank/whitespace `User` falls back to `multi-role`. 2. **No-op sign-in/out** — `SignInAsync`/`SignOutAsync` complete without throwing. 3. **Registration switch** — with the flag true the registered handler for the cookie scheme is `AutoLoginAuthenticationHandler`; with the flag false it is the cookie handler (assert via the resolved `IAuthenticationSchemeProvider` / scheme `HandlerType`). 4. (Optional) **Claim parity** — the handler's principal matches a real-login principal shape for an all-roles system-wide user (reuse the M2.19 `SessionClaimBuilderParityTests` idiom). ## Files (anticipated) - **New:** `src/ZB.MOM.WW.ScadaBridge.Security/Auth/AuthDisableLoginOptions.cs`, `.../Auth/AutoLoginAuthenticationHandler.cs`. - **Modify:** `src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs` (`All` array), `.../ServiceCollectionExtensions.cs` (flag param + scheme switch + warning + options bind), `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs` (read flag, pass to `AddSecurity`). - **Tests:** `tests/ZB.MOM.WW.ScadaBridge.Security.Tests/AutoLoginAuthenticationHandlerTests.cs` (+ a registration-switch test). - **Docs/deploy (optional):** note the flag in the Security component doc / docker-dev appsettings. ## Out of scope - Any production safety guard (explicitly declined). - Hiding/redirecting the login page (unnecessary — it is unreachable when auth always succeeds). - Bearer-token (`/auth/token`) path — unaffected; this is the cookie/interactive path only.