# Auth — gaps & adoption backlog

Divergence of each project from [`spec/SPEC.md`](spec/SPEC.md), and the ordered backlog to
reach the shared `ZB.MOM.WW.Auth` library. Status legend: ⛔ gap · 🟡 partial · ✅ matches.

> **`0.1.5` PUBLISHED 2026-07-18 — security fix.** `Auth.ApiKeys` `0.1.4` and earlier ship a
> high-severity transitive vuln: `Microsoft.Data.Sqlite 10.0.7` pulls
> `SQLitePCLRaw.lib.e_sqlite3 2.1.11` (GHSA-2m69-gcr7-jv3q). `0.1.5` pins the patched `2.1.12`
> (same approach as `ZB.MOM.WW.LocalDb`); the `.nuspec` declares it directly, so consumers get
> the fix on bump without pinning themselves. **No API change** — `0.1.5` is `0.1.4` plus the pin.
> **All three consumers bumped to `0.1.5` the same day** (mxaccessgw `e107019`, ScadaBridge
> `50d79ed1`, HistorianGateway `df211ee` — all pushed). Exposure was verified per repo rather
> than assumed: mxaccessgw and ScadaBridge genuinely resolved the vulnerable `2.1.11` and now
> resolve `2.1.12`; **HistorianGateway was never exposed** (already resolved `3.50.3` via another
> dependency), so its bump is consistency-only. ScadaBridge's `0.1.3`→`0.1.5` jump crosses the
> `0.1.4` `ExpiresUtc` change, but that was additive — no source changes were needed and its
> suites match baseline exactly (Security 181, InboundAPI 269). **OtOpcUa stays at `0.1.1`** and
> was never exposed: it takes no `ApiKeys`/SQLite dependency.
>
> ⚠️ **Caveat:** HistorianGateway's bump could NOT be test-verified — a pre-existing, unrelated
> `AngleSharp` NU1902 error blocks its test project under `TreatWarningsAsErrors` (ScadaBridge's
> `CentralUI.Tests` has the same break). Both confirmed present on clean trees beforehand.
>
> **Current state (verified 2026-07-18):** feed carries `0.1.0`–`0.1.5`; consumer pins are
> OtOpcUa `0.1.1`, ScadaBridge `0.1.3`, MxAccessGateway `0.1.4`, **HistorianGateway `0.1.4`**
> (a fourth consumer this file predates, taking all four packages). OtOpcUa is three minors
> behind — it does not get the `0.1.4` API-key-expiry enforcement. Library suite: **215 passing,
> 1 skipped**, green 2026-07-18.
>
> **✅ ADOPTED 2026-06-02 (local-only).** The full backlog (#1–#8) was implemented across all three apps on each repo's
> **`feat/adopt-zb-auth`** branch — committed + spec/code-reviewed, then **merged to each repo's local default
> (main/master) and PUSHED to origin (gitea) on 2026-06-03** (in sync; `feat/*` kept locally). Shared
> `Auth.Ldap` + `Auth.ApiKeys` (ScadaBridge inbound re-architected to keyId/Bearer), `IGroupRoleMapper<TRole>`,
> `Transport`-enum config, canonical `ZbClaimTypes`/`ZbCookieDefaults`, unified dev base DN `dc=zb,dc=local`, and the
> canonical-six roles (with ScadaBridge's accepted auditor/admin SoD collapse). Consumer pins: OtOpcUa `0.1.1`,
> MxGateway `0.1.2`, ScadaBridge `0.1.3`. Detail: `docs/plans/2026-06-02-auth-audit-normalization*.md`. The ⛔/🟡 cells
> below describe the PRE-adoption divergence (kept for history).

## Divergence vs spec

### §1 LDAP config schema

| Spec key | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| Section nesting | 🟡 `Authentication:Ldap` (nested) | ✅ `MxGateway:Ldap` (nested) | ⛔ flat `ScadaBridge:Security:Ldap*` |
| `Transport` enum | ⛔ `UseTls` bool | ⛔ `UseTls` bool | ✅ `LdapTransport` enum |
| `AllowInsecure` | 🟡 `AllowInsecureLdap` | 🟡 `AllowInsecureLdap` | 🟡 `AllowInsecureLdap` (rename) |
| `UserNameAttribute` | ✅ `UserNameAttribute` | ✅ `UserNameAttribute` | ⛔ `LdapUserIdAttribute` |
| `GroupAttribute` | ✅ `memberOf` | ✅ `memberOf` | 🟡 `LdapGroupAttribute` (rename) |
| dev `SearchBase` | `dc=lmxopcua,dc=local` | `dc=lmxopcua,dc=local` | `dc=scadabridge,dc=local` |

→ **Gap A1:** adopt the `Transport` enum in OtOpcUa + gateway (replace `UseTls`).
→ **Gap A2:** ScadaBridge: nest keys + rename `LdapUserIdAttribute`→`UserNameAttribute`, `LdapGroupAttribute`→`GroupAttribute`.
→ **Gap A3:** unify the **dev base DN** (`dc=lmxopcua` vs `dc=scadabridge`) — pick one shared GLAuth base.

### §2 bind-then-search

All three do bind-then-search; ScadaBridge has the most complete hygiene (RFC-4514 + filter
escaping, per-op timeout, fail-closed on group lookup, username trim, service-account-bind
distinction). 🟡 OtOpcUa/gateway: confirm each has filter escaping + fail-closed-on-group-lookup
parity. → **Gap B1:** make ScadaBridge's hygiene the shared baseline; backfill any missing checks.

### §3 group→role mapping

⛔ **Mechanism split:** OtOpcUa + gateway map in **config** (`GroupToRole`); ScadaBridge maps in
the **database** (`LdapGroupMapping`). → **Gap C1:** `IGroupRoleMapper<CanonicalRole>` must support both
backings; ship a config-backed and a DB/delegate-backed mapper.

⛔ **Role vocabulary now standardized** to the canonical six ([`spec/CANONICAL-ROLES.md`](spec/CANONICAL-ROLES.md));
native enforcement stays per-project. → **Gap C2:** implement the `canonical → native` expansion in each
project. ⚠ Removing Auditor collapses ScadaBridge `AuditReadOnly`→Viewer and `Audit`→Administrator,
losing its auditor/admin separation-of-duties (accepted). OtOpcUa lacks a first-class `Deployer`
(publish ⊂ `FleetAdmin`); ScadaBridge has no `Operator`/`Engineer`; mxaccessgw no `Designer`/`Deployer` —
each project assigns only the applicable subset.

### §4 API-key contract

| | OtOpcUa | MxAccessGateway | ScadaBridge |
|---|---|---|---|
| Has API keys | n/a (OPC UA transport security) | ✅ `mxgw_…`, SQLite, scopes + constraints | 🟡 `X-API-Key`, per-method approval (Inbound API only) |
| Peppered HMAC-SHA256 | — | ✅ | ✅ |
| Constant-time compare | — | ✅ | ✅ |
| Token format `<prefix>_<id>_<secret>` | — | ✅ | ⛔ raw `X-API-Key` (no keyId/prefix structure) |
| Audit log | — | ✅ append-only | 🟡 (verify) |

→ **Gap D1:** extract mxaccessgw's pipeline as `ZB.MOM.WW.Auth.ApiKeys`.
→ **Gap D2:** ScadaBridge Inbound API adopts it; reconcile token format and model "per-method approval" as the opaque constraint policy.

### §5 cookie / claim conventions

⛔ Cookie names differ (`MxGatewayDashboard` vs `ZB.MOM.WW.ScadaBridge.Auth` vs OtOpcUa control-plane cookie); claim-type conventions differ. → **Gap E1:** define canonical claim types + cookie defaults in `ZB.MOM.WW.Auth.AspNetCore`; each app keeps its own cookie *name* but shares attributes/claims.

### §6 dev / secrets

✅ All never-log-secrets and pepper-external. 🟡 escape-hatch flag names vary. Covered by A3 (base DN) + E.

## Adoption backlog (ordered)

| # | Item | Projects | Priority | Effort | Risk | Notes |
|---|---|---|---|---|---|---|
| 1 | Extract `ZB.MOM.WW.Auth.Ldap` from ScadaBridge's hardened impl | all 3 | High | M | Med | security-sensitive; needs strong tests before cutover |
| 2 | Extract `ZB.MOM.WW.Auth.ApiKeys` from mxaccessgw Model A | gw, SB | High | M | Med | gateway adopts first (it's the source), then SB |
| 3 | `IGroupRoleMapper<TRole>` seam + config & DB mappers (Gap C1) | all 3 | High | S | Low | unblocks per-project role retention |
| 4 | Config migration to §1 schema (Gaps A1–A2) | all 3 | Med | S | Low | mechanical; do with #1 |
| 5 | `ZB.MOM.WW.Auth.AspNetCore` claims/cookie conventions (Gap E1) | all 3 (UIs) | Med | S | Low | incl. OtOpcUa Blazor Admin UI control-plane |
| 6 | Unify dev GLAuth base DN (Gap A3) | all 3 | Low | S | Low | dev-only; touches fixtures/infra |
| 7 | Decide shared JWT/refresh helper vs per-project | SB (+?) | Low | S | Low | only if a 2nd project wants the same |
| 8 | Adopt canonical roles: `canonical → native` mapping per project (Gap C2) | all 3 | Med | M | Med | governance (assign canonical role per LDAP group org-wide) + each project's expansion; SB audit roles collapse |

**Sequencing:** #3 first (cheap, unblocks), then #1 and #2 in parallel (independent libraries),
then #4–#5 alongside cutover, then #6–#7 as cleanup. Each extraction lands behind tests in the
source project before any consumer migrates. This stays consistent with the repos' loose coupling:
adoption is opt-in per project, one consumer version-bump at a time.

## Decisions still open
- Shared dev base DN value (A3).
- Whether constraints stay opaque `object?` or get a small `IConstraintPolicy` (shared-contract Q3).
- Shared JWT/refresh helper or not (#7).
