Files
scadaproj/components/auth/shared-contract/ZB.MOM.WW.Auth.md
T

129 lines
6.9 KiB
Markdown

# 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<string> Groups, LdapAuthFailure? Failure);
public enum LdapAuthFailure { BadCredentials, UserNotFound, AmbiguousUser, GroupLookupFailed, ServiceAccountBindFailed, Disabled }
public interface ILdapAuthService { // §2
Task<LdapAuthResult> AuthenticateAsync(string username, string password, CancellationToken ct);
}
public enum CanonicalRole { Viewer, Operator, Engineer, Designer, Deployer, Administrator } // ../spec/CANONICAL-ROLES.md
public interface IGroupRoleMapper<TRole> { // §3 — TRole defaults to CanonicalRole; backing store stays per-project
Task<GroupRoleMapping<TRole>> MapAsync(IReadOnlyList<string> groups, CancellationToken ct);
}
public sealed record GroupRoleMapping<TRole>(IReadOnlyList<TRole> 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<ApiKeyVerification> 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<string> Scopes, object? Constraints);
public interface IApiKeyStore { // default: SQLite (hash, scopes, constraints, audit)
Task<ApiKeyRecord?> 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.