# Auth — normalized target spec Status: **Draft**. The single design the sister projects converge on. Derived from the three code-verified current-state docs (`../current-state/`). Goal is *path to shared code* (`../shared-contract/ZB.MOM.WW.Auth.md`), so each normalized section maps to a shared library seam. ## 0. Scope **Normalized here:** LDAP/identity config schema; bind-then-search behavior; the group→role mapping *seam*; **the standardized canonical role set every project maps onto ([`CANONICAL-ROLES.md`](CANONICAL-ROLES.md))**; the API-key contract; cookie/claim conventions; dev conventions; secret handling. **Explicitly NOT normalized** (domain-specific — keep per project): the authorization *enforcement* (OtOpcUa `NodePermissions` ACL trie, mxaccessgw gRPC scopes + constraints, ScadaBridge native roles + site-scoping) — each project **maps its native model onto the canonical roles** but keeps its own enforcement; OPC UA transport security; OtOpcUa's generation/staleness session model; ScadaBridge's site-scope rules; mxaccessgw's hub-token model. ## 1. LDAP / identity configuration schema One **nested `Ldap` options object**, bound under each app's own root section (`OtOpcUa:Authentication:Ldap`, `MxGateway:Ldap`, `ScadaBridge:Security:Ldap`). Canonical keys/types: | Key | Type | Notes | |---|---|---| | `Enabled` | bool | | | `Server` | string | host | | `Port` | int | 389 / 636 / 3893 (GLAuth dev) | | `Transport` | enum `Ldaps` \| `StartTls` \| `None` | **adopt ScadaBridge's enum** over a `UseTls` bool — it expresses StartTLS, which the bool can't | | `AllowInsecure` | bool (default false) | dev-only escape hatch; pairs with `Transport=None` | | `SearchBase` | string | base DN | | `ServiceAccountDn` | string | for bind-then-search | | `ServiceAccountPassword` | string | from secret store, never source | | `UserNameAttribute` | string | canonical name (default `cn` GLAuth / `sAMAccountName` AD). Supersedes ScadaBridge's `LdapUserIdAttribute` | | `DisplayNameAttribute` | string | default `cn` | | `GroupAttribute` | string | default `memberOf` | | `ConnectionTimeoutMs` | int | per-operation socket timeout | Migration: OtOpcUa `Authentication.Ldap.UseTls`→`Transport`; ScadaBridge flat `Ldap*`→nested + rename `LdapUserIdAttribute`→`UserNameAttribute`; gateway `MxGateway:Ldap` already close. ## 2. Bind-then-search behavior (canonical algorithm) 1. Connect to `Server:Port`. If `Transport != Ldaps/StartTls` and not `AllowInsecure` → **refuse** (config error). 2. Bind the **service account**; a failure here is a *system misconfiguration*, surfaced distinctly from bad user creds. 3. Search under `SearchBase` with filter `({UserNameAttribute}={escapedUsername})`. **Escape** the username (LDAP filter rules: `\ * ( ) NUL`). Reject **multiple** matches (ambiguous); no match → auth failure. 4. **Re-bind as the resolved user DN** with the supplied password to verify it. RFC 4514-escape DN components. 5. Read `GroupAttribute`; normalize group names (strip leading `CN=`/RDN). **If the group lookup fails, fail the login** — never admit a user with zero resolved groups. 6. **Trim-normalize** the username once at entry so one person ≠ two identities. Generic, injection-safe, fail-closed. (ScadaBridge's current impl is the closest reference.) ## 3. Group → role mapping seam Resolved LDAP groups (and API-key principals) map to the **standardized canonical role set** ([`CANONICAL-ROLES.md`](CANONICAL-ROLES.md)) via `IGroupRoleMapper`; each project then **expands a canonical role into its native permissions/scopes** and applies its own scope payload. The backing store is project-chosen — **config dict** (OtOpcUa `GroupToRole`, gateway `Dashboard:GroupToRole`) **or database** (ScadaBridge `LdapGroupMapping`). The shared library defines the seam + the `CanonicalRole` enum and ships both a config-backed and a delegate/DB-backed mapper; native enforcement stays per-project. ## 4. API-key contract (machine-to-machine) For projects with a programmatic surface (mxaccessgw gRPC, ScadaBridge Inbound API): - **Token:** `__` (prefix configurable, e.g. `mxgw`). Parsed/validated before any store hit. - **Hashing:** HMAC-SHA256 with an **external pepper** (from secret store/config, never stored beside the hash). Secret = 32 random bytes, URL-safe base64. - **Verify:** lookup by keyId → reject if revoked → hash presented secret with pepper → **constant-time compare** (`CryptographicOperations.FixedTimeEquals`). Discriminated failure reasons for audit; opaque error to the caller. - **Store:** abstraction with a default SQLite implementation (hash, scopes, optional constraints, created/last-used/revoked) + **append-only audit**. Revoke = timestamp; delete only when revoked. - **Scopes:** a `string` set gating operations (project supplies the catalog). - **Constraints:** an **opaque, project-supplied policy** object (mxaccessgw subtree/tag globs + `MaxWriteClassification`; ScadaBridge per-method approval). The contract carries it; it does not hard-code either shape. - **Admin CLI:** `init-db / create-key / list-keys / revoke-key / rotate-key / delete-key`. (mxaccessgw Model A is the reference implementation to extract.) ## 5. Cookie / claim / session conventions - **Cookie:** HttpOnly, `SameSite=Strict`, `Secure` configurable for dev (`RequireHttpsCookie`), sliding idle expiry. Name pattern `.Auth`. - **Claims:** canonical claim types for name, display name, username, role (one per role), and any project scope id — defined once in `ZB.MOM.WW.Auth.AspNetCore`. - **DataProtection** keys persisted to shared storage so cookies/tokens survive node failover (OtOpcUa already does this via Config DB). - Session *lifetime policy* (refresh windows, staleness, generation binding) stays per-project. ## 6. Dev & secret conventions - **Dev directory:** GLAuth. **Unify the dev base DN** — today OtOpcUa/gateway use `dc=lmxopcua,dc=local` and ScadaBridge uses `dc=scadabridge,dc=local`; pick one shared dev base DN (see `../GAPS.md`). - **Dev escape hatches** named consistently: `AllowInsecure` (LDAP), plus each project's documented bypass (`AllowAnonymousLocalhost`, `DevStubMode`) clearly dev-only and off in prod. - **Secrets:** service-account passwords, JWT signing keys, and API-key peppers come from a secret store / env, never source; never logged (API keys, passwords, secured payloads, credentials). ## 7. Acceptance (what "converged" means) A project is converged when: (a) its LDAP authn + (if applicable) API-key auth run on the `ZB.MOM.WW.Auth` packages; (b) its config matches §1's schema; (c) its group→role mapping implements the §3 seam; (d) cookie/claim conventions match §5; with all project-specific authorization unchanged.