The `!` marks the new package + version, not an API break — SQLite stays default and existing consumers need no code change. Claude-Session: https://claude.ai/code/session_013L3XdmVii9i5sY4FChU8pB
7.9 KiB
ZB.MOM.WW.Auth
Authentication and authorisation libraries for the ZB.MOM.WW SCADA family (OtOpcUa, MxAccessGateway, ScadaBridge). These are libraries, not a service — each package is linked directly into the consuming application at build time. There is no central authentication process or network hop; auth logic runs in-process alongside the application.
Packages
| Package | Description | Key Dependencies |
|---|---|---|
ZB.MOM.WW.Auth.Abstractions |
Auth contracts, canonical role constants, and shared types (LdapOptions, LdapAuthResult, ILdapAuthService, IApiKeyStore). No runtime dependencies beyond the BCL. |
— |
ZB.MOM.WW.Auth.Ldap |
LDAP authentication service: bind-then-search-then-bind against GLAuth or Active Directory; RFC 4514-aware group extraction; fail-closed. | Abstractions, Novell.Directory.Ldap.NETStandard |
ZB.MOM.WW.Auth.ApiKeys |
SQLite-backed API-key store with pepper-keyed HMAC-SHA256 secret hashing, rotation, and audit log. DI wiring is AddZbApiKeyAuth; an opt-in MigrationHostedService runs schema migrations on startup. |
Abstractions, Microsoft.Data.Sqlite |
ZB.MOM.WW.Auth.ApiKeys.SqlServer |
SQL Server-backed API-key store sharing one row set across clustered hosts (e.g. a central pair), instead of each node keeping its own SQLite file. DI wiring is AddZbApiKeyAuthSqlServer(config, apiKeySection, sqlServerSection), called instead of AddZbApiKeyAuth (it calls that internally). Also ships SqliteApiKeyStoreImporter — a one-shot per-node SQLite → shared store copy a host runs once at boot. |
Abstractions, ApiKeys, Microsoft.Data.SqlClient |
ZB.MOM.WW.Auth.AspNetCore |
ASP.NET Core wiring for the LDAP provider only: AddZbLdapAuth (binds + start-time-validates LdapOptions, registers ILdapAuthService), plus ZbCookieDefaults.Apply (hardened cookie helper the consumer calls itself) and ZbClaimTypes constants. It does not wire API keys or cookie middleware — API-key DI is AddZbApiKeyAuth in the ApiKeys package. |
Abstractions, Ldap, Microsoft.AspNetCore.App |
Consumer Matrix
| Consumer | Abstractions | Ldap | ApiKeys | ApiKeys.SqlServer | AspNetCore |
|---|---|---|---|---|---|
| OtOpcUa | yes | yes | — | — | yes |
| MxAccessGateway | yes | yes | yes | — | yes |
| ScadaBridge | yes | yes | yes | yes (central pair, 2026-09-02) | yes |
ApiKeys is NOT used by OtOpcUa (that app authenticates human operators via LDAP + cookies only; machine-to-machine access is out of scope).
Versioning
All five packages are versioned lockstep from Directory.Build.props. The current release is 0.3.0. A single version bump in Directory.Build.props bumps all five packages simultaneously — consumers should reference the same version for all ZB.MOM.WW.Auth packages.
0.3.0 — SQL Server API-key store (ScadaBridge central pair)
IApiKeyStoreMigratoradded toAbstractions: creates or upgrades an API-key store's schema; idempotent and safe to run concurrently from several nodes.SqliteAuthStoreMigratorimplements it unchanged.AddZbApiKeyAuthregisters the SQLite implementation viaTryAdd, so a provider that registers its own migrator first (see below) wins — that ordering is the whole mechanism by which a different backend is swapped in.- New package
ZB.MOM.WW.Auth.ApiKeys.SqlServer+AddZbApiKeyAuthSqlServer(config, apiKeySection, sqlServerSection), called instead ofAddZbApiKeyAuth(it calls it internally after registering the SQL Server stores and migrator first). zbauthschema v1 (three tables:schema_version,api_keys,api_key_audit;DATETIMEOFFSET(7)timestamps).SqlServerApiKeyStoreMigratoris concurrent-boot safe — a central pair's two nodes both migrate at every boot — with retry on SQL error numbers 1205/2714/1913/2627 and jittered backoff (measured: the flat backoff it replaced exhausted its retries roughly 1 in 60 eight-way boots). DBA-facing DDL:docs/sql/zbauth-schema.sql.SqliteApiKeyStoreImporter— idempotent insert-where-not-exists copy of an existing node's per-node SQLite keys and audit rows into the shared store; a row already present in SQL Server is never modified, so running it from both nodes of a pair converges on the union of their files.ApiKeyAdminCommandsgained a ctor takingIApiKeyStoreMigrator; the existing ctor taking the concreteSqliteAuthStoreMigratoris retained (MxAccessGateway, HistorianGateway and ScadaBridge all construct it that way).- SQLite remains the default store; existing consumers need no code change to move to 0.3.0.
- Live tests for the SQL Server package are gated on
AUTH_SQLSERVER_CONNSTRand skip (not fail) when it is unset.
0.2.1 — LDAP AD continuation-referral fix
NovellLdapConnection.Searchnow skips Active Directory continuation referrals (LdapReferralExceptionfromNext()) instead of letting one fail the whole search — aSearchBaseat a domain root made every login fail asServiceAccountBindFailedagainst a healthy directory. Non-referral faults still propagate untouched.
0.2.0 — version bump only
- No functional change; bumped the lockstep family version ahead of the 0.2.1 LDAP fix.
0.1.4 — API-key expiry (archreview G-2)
ApiKeyRecord/ApiKeyListItemgain an optionalDateTimeOffset? ExpiresUtc(nullable;NULL= never expires).ApiKeyFailuregainsKeyExpired.ApiKeyVerifierenforces expiry — a key whoseExpiresUtcis at or before "now" (inclusive, injected clock) fails withKeyExpired, decided before the secret comparison. Existing (never-expiring) keys are unaffected.ApiKeyAdminCommands.CreateKeyAsynchas a new overload takingDateTimeOffset? expiresUtc; the old signature delegates withnull.RotateKeyAsyncdeliberately preserves an existing expiry (rotation changes only the secret).- SQLite schema → v3: a nullable
expires_utccolumn, added to fresh DBs by the CREATE DDL and to existing v1/v2 DBs by an idempotentALTER TABLEin the migrator (existing rows →NULL). A donor v2gateway-auth.dbis accepted (2 ≤ 3) and upgraded in place — no key is invalidated. Old-build readers tolerate the extra column. - Consumers must bump to 0.1.4 to set/enforce expiry (mxaccessgw + HistorianGateway benefit from the same verifier).
Running the opt-in LDAP integration test
The GLAuth integration test (GLAuthIntegrationTests) is skipped by default and does not affect the normal test run. To exercise it against a live GLAuth instance:
-
Start the GLAuth Docker stack from the sibling repo:
cd ~/Desktop/ScadaBridge/infra/glauth docker compose up -d -
Set the required environment variables and run the test:
export ZB_LDAP_IT=1 export ZB_LDAP_SVC_DN="cn=svc,dc=lmxopcua,dc=local" export ZB_LDAP_SVC_PW="svcpass" export ZB_LDAP_USER="alice" export ZB_LDAP_PW="alicepass" dotnet test tests/ZB.MOM.WW.Auth.Ldap.Tests \ --filter "FullyQualifiedName~GLAuthIntegrationTests"
All other variables (ZB_LDAP_SERVER, ZB_LDAP_PORT, ZB_LDAP_BASE, ZB_LDAP_USERATTR) default to sensible GLAuth values and are optional. The test also probes TCP reachability before attempting auth and skips if the server is not contactable.
Publishing packages
Use build/push.sh to pack and push to the Gitea NuGet feed:
export GITEA_NUGET_SOURCE="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json"
export GITEA_NUGET_KEY="your-gitea-token"
./build/push.sh
The script runs dotnet pack -c Release then dotnet nuget push --skip-duplicate.
Design documentation
Full design docs live in the components/auth folder of the SCADA project notes:
~/Desktop/scadaproj/components/auth/spec/SPEC.md— overall auth specification~/Desktop/scadaproj/components/auth/spec/CANONICAL-ROLES.md— role taxonomy~/Desktop/scadaproj/components/auth/shared-contract/— shared contract types