Files
scadaproj/components/auth/current-state/mxaccessgw/CURRENT-STATE.md
T

6.2 KiB

Auth — current state: MxAccessGateway (mxaccessgw)

Repo: ~/Desktop/MxAccessGateway (Gitea mxaccessgw). Stack: .NET 10 gateway (x64) + x86/net48 worker. Auth lives entirely in the gateway (.NET 10); the worker never authenticates. All paths relative to repo root; auth code under src/ZB.MOM.WW.MxGateway.Server/. Verified 2026-06-01.

The gateway has two independent auth models: gRPC API keys (programmatic clients) and LDAP dashboard auth (web UI). They share nothing — different credentials, stores, and authz.

Model A — gRPC API-key auth

Base: Security/Authentication/ and Security/Authorization/.

  • Token format: Authorization: Bearer mxgw_<key-id>_<secret>. Parsed by ApiKeyParser.cs (rejects malformed before any DB hit) → ParsedApiKey(KeyId, Secret).
  • Hashing: ApiKeySecretHasher.cs — HMAC-SHA256 with an external pepper (config key MxGateway:ApiKeyPepper, never stored beside the hash). Secrets generated by ApiKeySecretGenerator.cs (32 random bytes, URL-safe base64).
  • Verification: ApiKeyVerifier.cs — parse → IApiKeyStore.FindByKeyIdAsync → reject if revoked → hash with pepper → constant-time compare (CryptographicOperations.FixedTimeEquals) → mark LastUsedUtc. Failures discriminated (KeyNotFound/KeyRevoked/PepperUnavailable/SecretMismatch/MissingOrMalformedCredentials) for audit without leaking to clients.
  • Storage: SQLite (AuthSqliteConnectionFactory.cs, WAL), default C:\ProgramData\MxGateway\gateway-auth.db. Schema v2 (SqliteAuthSchema.cs): tables api_keys (hash, scopes JSON, constraints JSON, created/last_used/revoked), api_key_audit (append-only), schema_version. Read/write/audit split across SqliteApiKeyStore / SqliteApiKeyAdminStore / SqliteApiKeyAuditStore.
  • Scopes (GatewayScopes.cs): session:open/close, invoke:read/write/secure, events:read, metadata:read, admin. Stored per key (ordinal-sorted JSON via ApiKeyScopeSerializer.cs).
  • Enforcement: Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs + GatewayGrpcScopeResolver.cs — per RPC: authenticate (Unauthenticated on failure) → resolve required scope → check (PermissionDenied if missing) → push ApiKeyIdentity into async-local IGatewayRequestIdentityAccessor.
  • Constraints (ApiKeyConstraints.cs + ConstraintEnforcer.cs): optional fine-grained limits — ReadSubtrees/WriteSubtrees/ReadTagGlobs/WriteTagGlobs/BrowseSubtrees (anchored case-insensitive globs), MaxWriteClassification, ReadAlarmOnly, ReadHistorizedOnly.
  • Admin CLI: apikey subcommand (ApiKeyAdminCliRunner.cs, ApiKeyAdminCommandLineParser.cs): init-db / create-key / list-keys / revoke-key / rotate-key / delete-key (+ constraint flags). delete only works on already-revoked keys.
  • Config: Configuration/AuthenticationOptions.csMxGateway:Authentication:{Mode(ApiKey|Disabled), SqlitePath, PepperSecretName, RunMigrationsOnStartup}.

Model B — Dashboard LDAP auth (Blazor)

Base: Dashboard/.

  • Login: DashboardAuthenticator.cs — connect (MxGateway:Ldap), bind service account, search user (({UserNameAttribute}={escaped}), RFC-escaped), re-bind as user DN to verify password, read memberOf. /login GET/POST with antiforgery (DashboardEndpointRouteBuilderExtensions.cs).
  • Group→role: MxGateway:Dashboard:GroupToRole (case-insensitive; tries full DN then leading RDN) → roles Admin / Viewer (DashboardRoles.cs). Zero matched roles ⇒ login denied.
  • Cookie: scheme MxGateway.Dashboard; cookie name MxGatewayDashboard (note: no __Host- prefix despite some docs); HttpOnly, SameSite=Strict, 8h sliding; SecurePolicy via MxGateway:Dashboard:RequireHttpsCookie. Config in DashboardServiceCollectionExtensions.cs.
  • SignalR hubs (/hubs/snapshot|alarms|events): policy accepts the cookie OR a 30-min Data-Protection bearer minted at GET /hubs/token (HubTokenService.cs, purpose …Dashboard.HubToken.v1; HubTokenAuthenticationHandler.cs also reads access_token query for WS upgrades).
  • Loopback bypass: MxGateway:Dashboard:AllowAnonymousLocalhost (default true) — DashboardAuthorizationHandler.cs.
  • LDAP config: Configuration/LdapOptions.csMxGateway:Ldap:{Enabled, Server, Port(3893), UseTls, AllowInsecureLdap, SearchBase(dc=lmxopcua,dc=local), ServiceAccountDn, ServiceAccountPassword, UserNameAttribute(cn), DisplayNameAttribute, GroupAttribute(memberOf)}. Dev users/groups in glauth.md (incl. the gateway-specific GwAdmin group).

Secrets & config

Pepper resolved from config (MxGateway:ApiKeyPepper), never stored with hashes. CLAUDE.md: API keys, passwords, WriteSecured payloads, AuthenticateUser creds never logged. Docs: docs/Authentication.md, docs/Authorization.md, docs/GatewayDashboardDesign.md, docs/DesignDecisions.md, glauth.md.

Notable limits / TODOs

EventsHub has no per-session ACL yet (any dashboard user can subscribe to any session); no reconnectable sessions; single event subscriber per session; authz is scope+constraint, not per-item ACL.


Adoption plan → ZB.MOM.WW.Auth

Replace with the shared library:

  • Model A is the reference implementation for ZB.MOM.WW.Auth.ApiKeys. The whole Security/Authentication/ key pipeline (parser, hasher, generator, verifier, SQLite store, audit, scope serializer, admin CLI) maps almost 1:1 onto the proposed contract — extract it here first and have ScadaBridge's Inbound API adopt the same package. Token prefix becomes configurable (mxgw_).
  • Model B LDAP: DashboardAuthenticator's bind-then-search → ZB.MOM.WW.Auth.Ldap; cookie/claim wiring → ZB.MOM.WW.Auth.AspNetCore. Migrate MxGateway:Ldap:* to the canonical config schema.

Keep bespoke:

  • gRPC scope catalog + GatewayGrpcAuthorizationInterceptor + constraint globs (domain authz).
  • Dashboard Admin/Viewer role meaning, hub-token specifics, loopback bypass.

Watch: align the API-key MaxWriteClassification/constraint model with ScadaBridge's per-method approval when extracting — they are different shapes of "scope a key"; the shared contract should carry constraints as an opaque, project-supplied policy rather than hard-coding either.