Files
ScadaBridge/docs/plans/2026-06-16-disable-login-design.md
T
Joseph Doherty 5cf2d1cb99 docs(plan): design for dev disable-login auto-login flag (port from OtOpcUa)
Faithful copy (warning only, no env guard); custom AuthenticationHandler under the
cookie scheme; reuses M2.19 SessionClaimBuilder for an all-roles system-wide principal.
2026-06-16 08:31:19 -04:00

8.7 KiB

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").
  • AutoLoginAuthenticationHandlerHandleAuthenticateAsync 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<AuthenticationSchemeOptions, AutoLoginAuthenticationHandler>(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<ILoggerFactory> 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:

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:

public static readonly string[] All = [Administrator, Designer, Deployer, Viewer];

3. Auto-login handler

AutoLoginAuthenticationHandler in ZB.MOM.WW.ScadaBridge.Security, deriving from SignInAuthenticationHandler<AuthenticationSchemeOptions> (so it satisfies the sign-in/sign-out handler contract). It reuses M2.19's SessionClaimBuilder.Build(...) for claim parity with a real login:

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<AuthenticationSchemeOptions, AutoLoginAuthenticationHandler>(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<AuthDisableLoginOptions>().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. HandlerHandleAuthenticateAsync 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/outSignInAsync/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.