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 byApiKeyParser.cs(rejects malformed before any DB hit) →ParsedApiKey(KeyId, Secret). - Hashing:
ApiKeySecretHasher.cs— HMAC-SHA256 with an external pepper (config keyMxGateway:ApiKeyPepper, never stored beside the hash). Secrets generated byApiKeySecretGenerator.cs(32 random bytes, URL-safe base64). - Verification:
ApiKeyVerifier.cs— parse →IApiKeyStore.FindByKeyIdAsync→ reject if revoked → hash with pepper → constant-time compare (CryptographicOperations.FixedTimeEquals) → markLastUsedUtc. Failures discriminated (KeyNotFound/KeyRevoked/PepperUnavailable/SecretMismatch/MissingOrMalformedCredentials) for audit without leaking to clients. - Storage: SQLite (
AuthSqliteConnectionFactory.cs, WAL), defaultC:\ProgramData\MxGateway\gateway-auth.db. Schema v2 (SqliteAuthSchema.cs): tablesapi_keys(hash, scopes JSON, constraints JSON, created/last_used/revoked),api_key_audit(append-only),schema_version. Read/write/audit split acrossSqliteApiKeyStore/SqliteApiKeyAdminStore/SqliteApiKeyAuditStore. - Scopes (
GatewayScopes.cs):session:open/close,invoke:read/write/secure,events:read,metadata:read,admin. Stored per key (ordinal-sorted JSON viaApiKeyScopeSerializer.cs). - Enforcement:
Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs+GatewayGrpcScopeResolver.cs— per RPC: authenticate (Unauthenticatedon failure) → resolve required scope → check (PermissionDeniedif missing) → pushApiKeyIdentityinto async-localIGatewayRequestIdentityAccessor. - Constraints (
ApiKeyConstraints.cs+ConstraintEnforcer.cs): optional fine-grained limits —ReadSubtrees/WriteSubtrees/ReadTagGlobs/WriteTagGlobs/BrowseSubtrees(anchored case-insensitive globs),MaxWriteClassification,ReadAlarmOnly,ReadHistorizedOnly. - Admin CLI:
apikeysubcommand (ApiKeyAdminCliRunner.cs,ApiKeyAdminCommandLineParser.cs):init-db / create-key / list-keys / revoke-key / rotate-key / delete-key(+ constraint flags).deleteonly works on already-revoked keys. - Config:
Configuration/AuthenticationOptions.cs→MxGateway: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, readmemberOf./loginGET/POST with antiforgery (DashboardEndpointRouteBuilderExtensions.cs). - Group→role:
MxGateway:Dashboard:GroupToRole(case-insensitive; tries full DN then leading RDN) → rolesAdmin/Viewer(DashboardRoles.cs). Zero matched roles ⇒ login denied. - Cookie: scheme
MxGateway.Dashboard; cookie nameMxGatewayDashboard(note: no__Host-prefix despite some docs); HttpOnly, SameSite=Strict, 8h sliding;SecurePolicyviaMxGateway:Dashboard:RequireHttpsCookie. Config inDashboardServiceCollectionExtensions.cs. - SignalR hubs (
/hubs/snapshot|alarms|events): policy accepts the cookie OR a 30-min Data-Protection bearer minted atGET /hubs/token(HubTokenService.cs, purpose…Dashboard.HubToken.v1;HubTokenAuthenticationHandler.csalso readsaccess_tokenquery for WS upgrades). - Loopback bypass:
MxGateway:Dashboard:AllowAnonymousLocalhost(defaulttrue) —DashboardAuthorizationHandler.cs. - LDAP config:
Configuration/LdapOptions.cs→MxGateway:Ldap:{Enabled, Server, Port(3893), UseTls, AllowInsecureLdap, SearchBase(dc=lmxopcua,dc=local), ServiceAccountDn, ServiceAccountPassword, UserNameAttribute(cn), DisplayNameAttribute, GroupAttribute(memberOf)}. Dev users/groups inglauth.md(incl. the gateway-specificGwAdmingroup).
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 wholeSecurity/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. MigrateMxGateway:Ldap:*to the canonical config schema.
Keep bespoke:
- gRPC scope catalog +
GatewayGrpcAuthorizationInterceptor+ constraint globs (domain authz). - Dashboard
Admin/Viewerrole 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.