Files
scadaproj/components/auth/spec/SPEC.md
T

6.7 KiB

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); 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.UseTlsTransport; ScadaBridge flat Ldap*→nested + rename LdapUserIdAttributeUserNameAttribute; gateway MxGateway:Ldap already close.

2. Bind-then-search behavior (canonical algorithm)

  1. Connect to Server:Port. If Transport != Ldaps/StartTls and not AllowInsecurerefuse (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) via IGroupRoleMapper<CanonicalRole>; 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>_<keyId>_<secret> (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.)

  • Cookie: HttpOnly, SameSite=Strict, Secure configurable for dev (RequireHttpsCookie), sliding idle expiry. Name pattern <App>.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.