# 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. > **βœ… 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`, > `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` 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 `__` | β€” | βœ… | β›” 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` 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).