# 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<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`:

```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<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:

```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<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. **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.
