# Proposed shared library: `ZB.MOM.WW.Auth` A contract on paper — the public surface to extract so the three projects stop re-implementing identity + API-key auth. Realizes [`../spec/SPEC.md`](../spec/SPEC.md). **Not yet created.** Reference implementations already exist: mxaccessgw Model A (API keys), ScadaBridge `LdapAuthService` (LDAP hygiene). ## Packages (.NET 10) ``` ZB.MOM.WW.Auth.Abstractions # interfaces, options, result records — the stable surface ZB.MOM.WW.Auth.Ldap # bind-then-search authn (§2) ZB.MOM.WW.Auth.ApiKeys # peppered-HMAC key auth + SQLite store (§4) ZB.MOM.WW.Auth.AspNetCore # cookie/claim/DI helpers (§5) — OtOpcUa Admin UI, gateway, ScadaBridge ``` All four are .NET 10, which all auth-bearing processes are (OtOpcUa server, mxaccessgw **gateway**, ScadaBridge central) — the x86/net48 mxaccessgw worker does no auth, so net48 multi-targeting is **not** required. Published to the Gitea NuGet feed; SemVer; one consumer bump per release. ## Packaging & distribution **Four NuGet packages, one DLL each**, on the Gitea NuGet feed, lockstep SemVer to start (one version across all four; split to independent versions only if churn diverges). These are **libraries** linked into each app and copied to its own `bin/` — there is **no central auth service**. The repos stay separate processes sharing *code*, not a runtime dependency (auth must run in-process anyway: OPC UA SDK callback, gRPC interceptor, ASP.NET middleware). Consumers reference only what they need: | Package (→ DLL) | Transitive deps | OtOpcUa | mxaccessgw | ScadaBridge | |---|---|---|---|---| | `…Auth.Abstractions` | none | ✅ | ✅ | ✅ | | `…Auth.Ldap` | LDAP client (e.g. `System.DirectoryServices.Protocols`) | ✅ | ✅ | ✅ | | `…Auth.ApiKeys` | `Microsoft.Data.Sqlite` | — | ✅ | ✅ | | `…Auth.AspNetCore` | ASP.NET Core | ✅ (Admin UI) | ✅ | ✅ | **Why OtOpcUa *does* take `.AspNetCore`:** it has two auth surfaces. Its OPC UA **data plane** (UserName tokens via the SDK impersonation callback + ACL trie) is not HTTP and uses only `.Ldap` + `.Abstractions` behind a bespoke `IOpcUaUserAuthenticator`. But its Blazor **Admin UI control plane** (cookie + JWT + DataProtection + authorization policies) *is* ASP.NET Core, so it shares the canonical claim/cookie conventions from `.AspNetCore`. Both surfaces share `.Ldap` for the bind. (`.ApiKeys` is the only package OtOpcUa skips — it has no API-key surface.) ## `ZB.MOM.WW.Auth.Abstractions` ```csharp public sealed record LdapOptions { // §1 canonical schema public bool Enabled { get; init; } = true; public string Server { get; init; } = "localhost"; public int Port { get; init; } = 3893; public LdapTransport Transport { get; init; } = LdapTransport.Ldaps; public bool AllowInsecure { get; init; } public string SearchBase { get; init; } = ""; public string ServiceAccountDn { get; init; } = ""; public string ServiceAccountPassword { get; init; } = ""; public string UserNameAttribute { get; init; } = "cn"; public string DisplayNameAttribute { get; init; } = "cn"; public string GroupAttribute { get; init; } = "memberOf"; public int ConnectionTimeoutMs { get; init; } = 10_000; } public enum LdapTransport { Ldaps, StartTls, None } public sealed record LdapAuthResult( // outcome of authn bool Succeeded, string Username, string DisplayName, IReadOnlyList Groups, LdapAuthFailure? Failure); public enum LdapAuthFailure { BadCredentials, UserNotFound, AmbiguousUser, GroupLookupFailed, ServiceAccountBindFailed, Disabled } public interface ILdapAuthService { // §2 Task AuthenticateAsync(string username, string password, CancellationToken ct); } public enum CanonicalRole { Viewer, Operator, Engineer, Designer, Deployer, Administrator } // ../spec/CANONICAL-ROLES.md public interface IGroupRoleMapper { // §3 — TRole defaults to CanonicalRole; backing store stays per-project Task> MapAsync(IReadOnlyList groups, CancellationToken ct); } public sealed record GroupRoleMapping(IReadOnlyList Roles, object? Scope); // Each project expands a CanonicalRole into its native permissions/scopes at enforcement time. ``` ## `ZB.MOM.WW.Auth.ApiKeys` ```csharp public sealed record ApiKeyOptions { // §4 public string TokenPrefix { get; init; } = "mxgw"; // configurable per project public string PepperSecretName { get; init; } = ""; // resolved from secret store, never stored public string SqlitePath { get; init; } = ""; public bool RunMigrationsOnStartup { get; init; } = true; } public interface IApiKeyVerifier { Task VerifyAsync(string authorizationHeader, CancellationToken ct); } public sealed record ApiKeyVerification(bool Succeeded, ApiKeyIdentity? Identity, ApiKeyFailure? Failure); public enum ApiKeyFailure { MissingOrMalformed, KeyNotFound, KeyRevoked, PepperUnavailable, SecretMismatch } public sealed record ApiKeyIdentity(string KeyId, string DisplayName, IReadOnlySet Scopes, object? Constraints); public interface IApiKeyStore { // default: SQLite (hash, scopes, constraints, audit) Task FindByKeyIdAsync(string keyId, CancellationToken ct); Task MarkUsedAsync(string keyId, CancellationToken ct); } public interface IApiKeyAdminStore { /* create / revoke / rotate / delete + audit */ } ``` - Constraints are carried as an **opaque `object`** (project supplies the policy: mxaccessgw globs/classification, ScadaBridge per-method approval). The library does the parse→lookup→peppered-HMAC→constant-time-compare→audit pipeline; it does **not** interpret constraints. - Ships the `apikey` admin verbs as a reusable command set. ## `ZB.MOM.WW.Auth.AspNetCore` - Canonical `ClaimTypes` constants (name, display, username, role, scope-id). - Cookie defaults per §5 (HttpOnly, SameSite=Strict, configurable Secure, sliding idle). - DI helpers: `AddZbLdapAuth(IConfiguration)`, `AddZbApiKeyAuth(IConfiguration)`. ## What stays in each consumer OtOpcUa: `IOpcUaUserAuthenticator` adapter, ACL trie, transport security, session model. mxaccessgw: gRPC scope catalog + interceptor, constraint globs, hub tokens. ScadaBridge: role set + site-scoping, `ManagementActor` enforcement, JWT refresh policy. ## Open contract questions 1. **Group→role store** must support both config and DB backings without leaking either (the `object? Scope` payload covers site-scoping). Validate against ScadaBridge's union semantics. 2. **JWT/refresh**: shared helper or per-project? Only ScadaBridge has the 15-min refresh model today; OtOpcUa has cookie+JWT control plane. Decide when 2+ projects want the same shape. 3. **Constraint opacity**: confirm the `object?` boundary is enough, or whether a small `IConstraintPolicy` interface is cleaner. See [`../GAPS.md`](../GAPS.md) for the adoption order and effort/risk.