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.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user