Files
mxaccessgw/docs/audit/fragments/04-auth.md
T

31 KiB
Raw Blame History

Cluster 04 — Auth

Auditor: Claude Code (claude-sonnet-4-6) Date: 2026-06-03 Docs audited: docs/Authentication.md, docs/Authorization.md, glauth.md Code verified against: src/ZB.MOM.WW.MxGateway.Server/Security/** and Dashboard/**


DOC / Authentication.md / LINES 253271 CLAIM / AuthStoreServiceCollectionExtensions.AddSqliteAuthStore wires services via direct AddSingleton calls for IApiKeyParser, IApiKeySecretHasher, IApiKeyVerifier, IApiKeyStore/SqliteApiKeyStore, IApiKeyAdminStore/SqliteApiKeyAdminStore, IApiKeyAuditStore/SqliteApiKeyAuditStore, AuthSqliteConnectionFactory, IAuthStoreMigrator/SqliteAuthStoreMigrator, AuthStoreMigrationHostedService. CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — the shared library ZB.MOM.WW.Auth.ApiKeys is registered via services.AddZbApiKeyAuth(effectiveConfig, AuthenticationSectionPath), which owns all of those types. The local method no longer registers them individually. The doc code block is a fabricated snapshot of pre-migration code that no longer matches any method in the codebase. CODE_AREA / auth.apikeys SEVERITY / high PROPOSED_FIX / Replace the Registration section code block with the actual method body from AuthStoreServiceCollectionExtensions.cs (calls AddZbApiKeyAuth, then registers CanonicalForwardingApiKeyAuditStore, SqliteCanonicalAuditStore, IAuditWriter, ApiKeyAdminCommands, ApiKeyAdminCliRunner). Remove the statement that AddSqliteAuthStore "registers the migration hosted service" — the hosted service is registered by AddZbApiKeyAuth, not by local code.


DOC / Authentication.md / LINES 5368 CLAIM / ApiKeySecretHasher (registered behind IApiKeySecretHasher) hashes secrets with HMACSHA256 keyed by a server-side pepper. The pepper is resolved by IConfiguration lookup against PepperSecretName. ApiKeyPepperUnavailableException is thrown when the pepper is missing. CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:58 — these types (ApiKeySecretHasher, IApiKeySecretHasher, ApiKeyPepperUnavailableException) now live in the shared package ZB.MOM.WW.Auth.ApiKeys (PackageReference in .csproj line 11). The behavior is correct but the doc presents them as if they are local gateway types. The interceptor's return type is ApiKeyVerification not ApiKeyVerificationResult (AuthStoreServiceCollectionExtensions.cs context; GatewayGrpcAuthorizationInterceptor.cs:69). CODE_AREA / auth.apikeys SEVERITY / medium PROPOSED_FIX / Clarify that ApiKeySecretHasher, IApiKeySecretHasher, and ApiKeyPepperUnavailableException are provided by the ZB.MOM.WW.Auth.ApiKeys shared library, not gateway-local types. Correct ApiKeyVerificationResultApiKeyVerification (the type returned by IApiKeyVerifier.VerifyAsync in the interceptor).


DOC / Authentication.md / LINES 7298 CLAIM / ApiKeyVerifier (IApiKeyVerifier) step 5: "Compare hashes with CryptographicOperations.FixedTimeEquals." Step 6: "Record a LastUsedUtc timestamp via MarkKeyUsedAsync and return an ApiKeyIdentity." Code block shows ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch) and ApiKeyVerificationResult.Success(new ApiKeyIdentity(...)). CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs:69 — the interceptor receives ApiKeyVerification verification, not ApiKeyVerificationResult. These types are from the shared package ZB.MOM.WW.Auth.ApiKeys which was migrated to. The types, method signatures, and return types shown in the code block may have been renamed or restructured during the migration to the shared library; the gateway no longer owns or contains these implementations. CODE_AREA / auth.apikeys SEVERITY / medium PROPOSED_FIX / Update type names to match the shared library (ApiKeyVerification instead of ApiKeyVerificationResult). Add note that ApiKeyVerifier is from ZB.MOM.WW.Auth.ApiKeys. Verify failure enum values against the shared library.


DOC / Authentication.md / LINES 108122 CLAIM / "AuthSqliteConnectionFactory reads GatewayOptions.Authentication.SqlitePath" CLAIM_TYPE / term VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — AuthSqliteConnectionFactory is now registered by AddZbApiKeyAuth from the shared package. The doc implies it is a local type that reads the gateway's GatewayOptions, but it is actually from ZB.MOM.WW.Auth.ApiKeys and reads ApiKeyOptions.SqlitePath (bound from MxGateway:Authentication section). The behavior is equivalent but the doc is misleading about the type ownership. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / Note that AuthSqliteConnectionFactory is from ZB.MOM.WW.Auth.ApiKeys and reads ApiKeyOptions.SqlitePath (bound via MxGateway:Authentication:SqlitePath).


DOC / Authentication.md / LINES 126133 CLAIM / "SqliteAuthSchema declares table names and the current schema version as constants. Three tables are involved: api_keys, api_key_audit, schema_version." CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:6974 — a new audit_event table now exists in the same SQLite file, written by SqliteCanonicalAuditStore. The api_key_audit table is left in place but nothing writes to it once the CanonicalForwardingApiKeyAuditStore adapter overrides the library's audit store. The doc says only three tables; there are now at minimum four. CODE_AREA / auth.apikeys SEVERITY / medium PROPOSED_FIX / Add audit_event as a fourth table (from SqliteCanonicalAuditStore). Note that api_key_audit is retained by the schema but is no longer written to at runtime (the CanonicalForwardingApiKeyAuditStore adapter redirects all writes to audit_event via IAuditWriter).


DOC / Authentication.md / LINES 134153 CLAIM / "SqliteApiKeyStore (IApiKeyStore) handles the two reads needed at request time: FindByKeyIdAsync and FindActiveByKeyIdAsync. MarkKeyUsedAsync updates last_used_utc only for non-revoked rows." Shows ApiKeyRecordReader.Read code block with column-ordinal reader. CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — SqliteApiKeyStore is in the shared package ZB.MOM.WW.Auth.ApiKeys. The code block shown is from the package, not local gateway code. If the package's internal implementation has changed, the doc may be inaccurate. The doc presents this as if it is local gateway source. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / Clarify that SqliteApiKeyStore, ApiKeyRecord, and ApiKeyRecordReader are in the shared ZB.MOM.WW.Auth.ApiKeys package and are not directly modifiable in this repository. Remove or label the code block as "from shared library."


DOC / Authentication.md / LINES 156164 CLAIM / "SqliteApiKeyAdminStore (IApiKeyAdminStore) implements administrative mutations: CreateAsync, RevokeAsync, RotateAsync, DeleteAsync." CLAIM_TYPE / term VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:67 — SqliteApiKeyAdminStore is in ZB.MOM.WW.Auth.ApiKeys. The gateway now wraps admin operations through ApiKeyAdminCommands (from the same package), not by injecting IApiKeyAdminStore directly in the CLI runner. DashboardSnapshotService and DashboardApiKeyManagementService do consume IApiKeyAdminStore directly, which is fine. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / Note that SqliteApiKeyAdminStore is from the shared library. Note that the gateway CLI runner delegates through ApiKeyAdminCommands (shared library), not by calling IApiKeyAdminStore directly.


DOC / Authentication.md / LINES 165183 CLAIM / "SqliteAuthStoreMigrator executes the migration inside a single transaction so a partial failure leaves the database untouched, refuses to start when the on-disk schema version is newer than the binary supports, and idempotently creates the v1 schema." "Operators who manage schema out-of-band can disable the hosted run and use the admin CLI's init-db command instead." CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:104 — SqliteAuthStoreMigrator is from ZB.MOM.WW.Auth.ApiKeys (resolved via sp.GetRequiredService<SqliteAuthStoreMigrator>()). The description of its behavior is likely still accurate but is presented as locally-owned code. AuthStoreMigrationHostedService is also from the shared package (registered by AddZbApiKeyAuth). The code block shown at lines 171179 is from the package. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / Clarify that SqliteAuthStoreMigrator, IAuthStoreMigrator, and AuthStoreMigrationHostedService are from the shared library.


DOC / Authentication.md / LINES 187208 CLAIM / CLI subcommand table lists: init-db, create-key, list-keys, revoke-key, rotate-key. CLI example uses mxgateway apikey create-key --key-id ops.alice --display-name "Alice (ops)" --scopes read,write. CLAIM_TYPE / command VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayScopes.cs:513 — GatewayScopes.All contains session:open, session:close, invoke:read, invoke:write, invoke:secure, events:read, metadata:read, admin. The values read and write are not in the scope catalog. ApiKeyAdminCommandLineParser.ValidateScopes at line 170177 would reject --scopes read,write as unknown scopes. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / Replace --scopes read,write with valid scope strings, e.g. --scopes invoke:read,invoke:write. Update all CLI examples in Authentication.md to use canonical scope strings from GatewayScopes.All.


DOC / Authentication.md / LINES 229248 CLAIM / "ApiKeyScopeSerializer.Serialize writes a JSON array sorted with StringComparer.Ordinal." Code block shown. CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:5 — ApiKeyScopeSerializer is from the shared ZB.MOM.WW.Auth.ApiKeys package. The behavior described is likely correct but is presented as local gateway code. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / Note that ApiKeyScopeSerializer is in the shared ZB.MOM.WW.Auth.ApiKeys library.


DOC / Authorization.md / LINES 107113 CLAIM / Scope resolver code block includes TestConnectionRequest or GetLastDeployTimeRequest or DiscoverHierarchyRequest or WatchDeployEventsRequest => GatewayScopes.MetadataRead. CLAIM_TYPE / rpc/proto VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:2328 — the actual resolver also includes BrowseChildrenRequest => GatewayScopes.MetadataRead in the same arm. BrowseChildrenRequest was added (per docs/plans/2026-05-28-lazy-browse-implementation.md) but the code block in Authorization.md was not updated. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / Add BrowseChildrenRequest to the MetadataRead arm of the scope resolver code block. Update the scope catalog table at line 212 to include GalaxyRepository.BrowseChildren in the MetadataRead row.


DOC / Authorization.md / LINE 212 CLAIM / Scope catalog table row: MetadataRead / metadata:read / "MxCommandKind.ArchestraUserToId, MxCommandKind.GetSessionState, MxCommandKind.GetWorkerInfo, GalaxyRepository.TestConnection, GalaxyRepository.GetLastDeployTime, GalaxyRepository.DiscoverHierarchy, GalaxyRepository.WatchDeployEvents". CLAIM_TYPE / rpc/proto VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:27 — BrowseChildrenRequest is also mapped to metadata:read but is absent from the table. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / Add GalaxyRepository.BrowseChildren to the MetadataRead row of the scope catalog table.


DOC / Authorization.md / LINES 260270 CLAIM / Registration code block for AddGatewayGrpcAuthorization shows three AddSingleton calls: GatewayGrpcScopeResolver, IGatewayRequestIdentityAccessor/GatewayRequestIdentityAccessor, GatewayGrpcAuthorizationInterceptor, then AddGrpc. CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs:1831 — the actual method also registers IConstraintEnforcer/ConstraintEnforcer as a singleton (line 20) and configures GrpcServiceOptions with MaxReceiveMessageSize/MaxSendMessageSize from MxGateway:Protocol. The doc code block omits both. CODE_AREA / auth.scopes SEVERITY / medium PROPOSED_FIX / Update the Registration code block to include services.AddSingleton<IConstraintEnforcer, ConstraintEnforcer>() and the AddOptions<GrpcServiceOptions> configuration block for message size limits.


DOC / Authorization.md / LINE 273 CLAIM / "none of the three classes hold per-request state on instance fields" CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs:20 — there are now four singleton classes registered by AddGatewayGrpcAuthorization (GatewayGrpcScopeResolver, GatewayRequestIdentityAccessor, GatewayGrpcAuthorizationInterceptor, ConstraintEnforcer), not three. CODE_AREA / auth.scopes SEVERITY / low PROPOSED_FIX / Update "three classes" to "four classes."


DOC / glauth.md / LINES 6366 CLAIM / "LdapOptions.RequiredGroup defaults to GwAdmin, so the dashboard login and DashboardLdapLiveTests require admin to be a member of a GwAdmin group." CLAIM_TYPE / config-key VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs — no RequiredGroup field exists on the gateway's LdapOptions. The gateway enforces group membership via MxGateway:Dashboard:GroupToRole (a dictionary mapping LDAP group names to dashboard roles) in DashboardOptions. Authorization succeeds if the user's LDAP groups map to at least one role — there is no RequiredGroup concept in the current architecture. CODE_AREA / auth.ldap SEVERITY / high PROPOSED_FIX / Remove the sentence "LdapOptions.RequiredGroup defaults to GwAdmin." Replace with: the dashboard enforces that at least one of the user's LDAP groups appears in MxGateway:Dashboard:GroupToRole (e.g. GwAdmin: Administrator); a login with no matching group is rejected. DashboardLdapLiveTests seeds the role map with GwAdmin -> Administrator.


DOC / glauth.md / LINES 181182 CLAIM / "the authenticator strips to GwAdmin and matches against RequiredGroup" CLAIM_TYPE / behavior-rule VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs:3548 — the shared ILdapAuthService strips the leading RDN value from each group DN, and the gateway's DashboardGroupRoleMapper looks up the short name in GroupToRole. There is no RequiredGroup property or concept anywhere in the codebase. CODE_AREA / auth.ldap SEVERITY / high PROPOSED_FIX / Replace "matches against RequiredGroup" with "looks up the short RDN name (e.g. GwAdmin) in MxGateway:Dashboard:GroupToRole."


DOC / glauth.md / LINES 113136 CLAIM / "Suggested mxgw configuration shape" YAML block uses config keys useTls, allowInsecureLdap, userNameAttribute. CLAIM_TYPE / config-key VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:49,52,64 — the current config keys (as bound by the shared LdapOptions and the gateway's shadow LdapOptions) are Transport (an enum: None/Ldaps/StartTls), AllowInsecure (bool), UserNameAttribute (string, default "cn" not "uid"). The YAML block uses stale camelCase key names from a pre-migration configuration shape. CODE_AREA / auth.ldap SEVERITY / high PROPOSED_FIX / Update the YAML config example to use Transport: None (or Ldaps/StartTls) instead of useTls: false, AllowInsecure: true instead of allowInsecureLdap: true, UserNameAttribute: "cn" (gateway default; note GLAuth populates cn not uid per the gateway default). Rename the section header from ldap: to MxGateway: Ldap: to match the actual config path.


DOC / glauth.md / LINE 128 CLAIM / userNameAttribute: "uid" # GLAuth populates this; AD uses sAMAccountName CLAIM_TYPE / config-key VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:64 — the gateway LdapOptions default for UserNameAttribute is "cn", not "uid". GLAuth does populate both uid and cn, but the gateway ships "cn" as default. CODE_AREA / auth.ldap SEVERITY / medium PROPOSED_FIX / Change example to UserNameAttribute: "cn" with a note that the gateway default is cn; to use uid instead set MxGateway:Ldap:UserNameAttribute: uid.


DOC / glauth.md / LINES 261269 CLAIM / AD migration cheat-sheet uses field names UseTls and AllowInsecureLdap. CLAIM_TYPE / config-key VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:49,52 — these fields were renamed: UseTlsTransport (enum), AllowInsecureLdapAllowInsecure. CODE_AREA / auth.ldap SEVERITY / high PROPOSED_FIX / Update the AD migration table: rename UseTls row to Transport (GLAuth dev value: None, AD value: Ldaps); rename AllowInsecureLdap row to AllowInsecure (GLAuth dev: true, AD: false).


DOC / CLAUDE.md / LINE 119 CLAIM / "maps the user's LDAP groups to Admin or Viewer via MxGateway:Dashboard:GroupToRole, then issues an HTTP-only secure __Host-MxGatewayDashboard cookie" CLAIM_TYPE / term VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationDefaults.cs:38 — the cookie name constant is CookieName = "MxGatewayDashboard" (no __Host- prefix). __Host- is a browser security prefix that requires Path=/, no Domain, and Secure — the code sets Path = "/" and SecurePolicy = Always by default, satisfying the requirements, but the actual cookie name in the constant and in ZbCookieDefaults.Apply is MxGatewayDashboard, not __Host-MxGatewayDashboard. Additionally, Admin should be Administrator (the renamed role value per DashboardRoles.Admin = "Administrator"). CODE_AREA / auth.cookie SEVERITY / high PROPOSED_FIX / Change __Host-MxGatewayDashboard to MxGatewayDashboard in CLAUDE.md. Change Admin to Administrator.


DOC / CLAUDE.md / LINE 119 CLAIM / "maps the user's LDAP groups to Admin or Viewer" CLAIM_TYPE / term VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs:14 — DashboardRoles.Admin = "Administrator" (not "Admin"). The role value was renamed in Task 1.7. CLAUDE.md was not updated. CODE_AREA / auth.roles SEVERITY / high PROPOSED_FIX / Change Admin to Administrator in the CLAUDE.md authentication paragraph.


DOC / CLAUDE.md / LINE 35 CLAIM / dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,invoke,event,metadata,admin CLAIM_TYPE / command VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayScopes.cs:513 — canonical scopes are session:open, session:close, invoke:read, invoke:write, invoke:secure, events:read, metadata:read, admin. The shorthand values session, invoke, event, metadata are not recognized and would be rejected by ApiKeyAdminCommandLineParser.ValidateScopes as unknown scopes. Also, the subcommand is create-key not create. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / Replace the example with a valid invocation, e.g.: dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create-key --key-id dev --display-name "dev" --scopes session:open,session:close,invoke:read,invoke:write,events:read,metadata:read,admin


DOC / CLAUDE.md / LINE 117 CLAIM / "Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default C:\ProgramData\MxGateway\gateway-auth.db). Scopes (session, invoke, event, metadata, admin) gate specific RPCs" CLAIM_TYPE / config-key VERDICT / wrong EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/AuthenticationOptions.cs:9 — SQLite path default is correct. However, scope names session, invoke, event, metadata are not the canonical scope strings. Actual scopes are session:open, session:close, invoke:read, invoke:write, invoke:secure, events:read, metadata:read, admin. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / Replace the scope shorthand list with the full canonical scope strings from GatewayScopes.All. The SQLite path is accurate and should be kept.


DOC / glauth.md / LINES 7074 CLAIM / "> Dashboard role value (Task 1.7): the LDAP GwAdmin group now maps to the canonical dashboard role Administrator (was Admin); GwReader maps to Viewer." CLAIM_TYPE / term VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs:14 — DashboardRoles.Admin = "Administrator", DashboardRoles.Viewer = "Viewer". src/ZB.MOM.WW.MxGateway.Server/appsettings.json:6364 confirms "GwAdmin": "Administrator", "GwReader": "Viewer". CODE_AREA / auth.roles SEVERITY / low PROPOSED_FIX / flag only


DOC / glauth.md / LINES 2126 CLAIM / Connection details: Protocol LDAP, Host localhost, Port 3893, Base DN dc=zb,dc=local, Bind DN format cn={username},dc=zb,dc=local, Group OU ou=<groupname>,ou=groups,dc=zb,dc=local. CLAIM_TYPE / config-key VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/LdapOptions.cs:36,39,55,58 — defaults: Server=localhost, Port=3893, SearchBase=dc=zb,dc=local, ServiceAccountDn=cn=serviceaccount,dc=zb,dc=local. CODE_AREA / auth.ldap SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authentication.md / LINES 130 CLAIM / Token format mxgw_<keyId>_<secret>, prefix mxgw_, parser is ApiKeyParser behind IApiKeyParser. CLAIM_TYPE / term VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:30,33 — TokenPrefix = "mxgw", PepperSecretName = "MxGateway:ApiKeyPepper". The token format claim is accurate; IApiKeyParser/ApiKeyParser are from the shared package but the behavior description matches. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authentication.md / LINE 110 CLAIM / "AuthSqliteConnectionFactory reads GatewayOptions.Authentication.SqlitePath" CLAIM_TYPE / config-key VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/AuthenticationOptions.cs:9 — SqlitePath default is C:\ProgramData\MxGateway\gateway-auth.db. The factory reads from ApiKeyOptions.SqlitePath which is bound from MxGateway:Authentication:SqlitePath, so the effective config key path matches GatewayOptions.Authentication.SqlitePath. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authentication.md / LINES 189208 CLAIM / CLI subcommands: init-db, create-key, list-keys, revoke-key, rotate-key. CLAIM_TYPE / command VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCommandKind.cs — enum has InitDb, CreateKey, ListKeys, RevokeKey, RotateKey. ApiKeyAdminCommandLineParser.cs maps these to exactly those string values. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authentication.md / LINES 220225 CLAIM / "Every destructive dashboard action is gated by a confirmation dialog and emits its own audit event (dashboard-create-key, dashboard-rotate-key, dashboard-revoke-key, dashboard-delete-key)." CLAIM_TYPE / behavior-rule VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs:69,201 — audit event strings dashboard-create-key and dashboard-delete-key confirmed in code. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authorization.md / LINES 94116 CLAIM / Scope resolver switches on request type; _ => GatewayScopes.Admin fallback for unrecognized types. CLAIM_TYPE / behavior-rule VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:1329 — the pattern and fallback match exactly. CODE_AREA / auth.scopes SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authorization.md / LINE 85 CLAIM / "If GatewayOptions.Authentication.Mode is AuthenticationMode.Disabled, the helper returns null immediately. No identity is pushed onto the accessor and the continuation runs without scope enforcement. This matches the AuthenticationMode enum, which only defines ApiKey and Disabled." CLAIM_TYPE / behavior-rule VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs:59 — confirmed. CODE_AREA / auth.apikeys SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authorization.md / LINE 215 CLAIM / "The Admin constant is also referenced by DashboardAuthenticator and DashboardAuthorizationHandler so that the dashboard and the gRPC layer agree on what 'admin' means." CLAIM_TYPE / behavior-rule VERDICT / stale EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs — DashboardAuthenticator does not reference GatewayScopes.Admin. The admin gRPC scope and the Administrator dashboard role are separate concepts. The dashboard authorization policy uses DashboardRoles.Admin = "Administrator", not GatewayScopes.Admin = "admin". These are distinct and do not share a constant. CODE_AREA / auth.roles SEVERITY / medium PROPOSED_FIX / Correct or remove the claim that GatewayScopes.Admin is referenced by DashboardAuthenticator. The dashboard and gRPC "admin" are deliberately separate concepts — the dashboard role is Administrator (a role claim value on the ClaimsPrincipal), while the gRPC scope is the literal string "admin" (a scope string on ApiKeyIdentity).


DOC / docs/Authorization.md / LINE 116 CLAIM / "AcknowledgeAlarm is treated as a write — it mutates alarm state, mirroring MxCommandKind.Write* — and StreamAlarms shares the alarm/event surface with StreamEvents and MxCommandKind.DrainEvents, so it carries events:read. Both alarm RPCs are session-less." CLAIM_TYPE / behavior-rule VERDICT / accurate EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs:21,22 — AcknowledgeAlarmRequest => GatewayScopes.InvokeWrite, StreamAlarmsRequest => GatewayScopes.EventsRead. Confirmed. CODE_AREA / auth.scopes SEVERITY / low PROPOSED_FIX / flag only


DOC / docs/Authorization.md / LINES 205215 CLAIM / Scope catalog table — all scope strings and their Required For mappings. CLAIM_TYPE / rpc/proto VERDICT / stale EVIDENCE / GatewayGrpcScopeResolver.cs:27 — BrowseChildrenRequest is missing from the MetadataRead row (already captured above). All other rows are accurate. CODE_AREA / auth.scopes SEVERITY / high PROPOSED_FIX / (Same as finding above — add GalaxyRepository.BrowseChildren to MetadataRead row.)


GAP FINDINGS (auth behavior in code but undocumented)

DOC / (none — gap) CLAIM / DashboardAuthenticationDefaults.CookieName is the default cookie name "MxGatewayDashboard", but DashboardOptions.CookieName allows a per-deployment override via MxGateway:Dashboard:CookieName. Auth docs do not mention this override. CLAIM_TYPE / config-key VERDICT / gap EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:9197, src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs:33. CODE_AREA / auth.cookie SEVERITY / medium PROPOSED_FIX / Add documentation of MxGateway:Dashboard:CookieName override and when to use it (multiple gateway instances sharing a hostname).


DOC / (none — gap) CLAIM / The dashboard cookie idle timeout is 8 hours (set by ZbCookieDefaults.Apply with idleTimeout: TimeSpan.FromHours(8)). The hub bearer token expires in 30 minutes (HubTokenService.TokenLifetime = TimeSpan.FromMinutes(30)). Neither timeout is documented in Authentication.md. CLAIM_TYPE / behavior-rule VERDICT / gap EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:66, src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs:29. CODE_AREA / auth.hub SEVERITY / medium PROPOSED_FIX / Add a section in Authentication.md (or GatewayDashboardDesign.md) documenting the 8-hour dashboard cookie idle timeout and the 30-minute hub bearer token lifetime.


DOC / (none — gap) CLAIM / The CanonicalForwardingApiKeyAuditStore overrides the shared library's IApiKeyAuditStore. As a result, the api_key_audit table in the SQLite DB is written by the shared library's migration but is NOT written to at runtime — all audit records go to audit_event via IAuditWriter. This is operationally important for anyone reading the DB directly but is not documented. CLAIM_TYPE / behavior-rule VERDICT / gap EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs:8594, src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs. CODE_AREA / auth.apikeys SEVERITY / medium PROPOSED_FIX / Document in Authentication.md that api_key_audit exists in the schema but is unused at runtime; all audit events flow to audit_event via IAuditWriter/SqliteCanonicalAuditStore.


DOC / (none — gap) CLAIM / DashboardOptions.RequireHttpsCookie (default true) controls whether the dashboard cookie uses SecurePolicy.Always or SameAsRequest. Setting it false is required for plain-HTTP dev deployments. This config key is not mentioned in auth docs. CLAIM_TYPE / config-key VERDICT / gap EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions.cs:22, src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:87. CODE_AREA / auth.cookie SEVERITY / low PROPOSED_FIX / Reference MxGateway:Dashboard:RequireHttpsCookie in the auth cookie documentation.


DOC / (none — gap) CLAIM / ZbClaimTypes and ZbCookieDefaults (from ZB.MOM.WW.Auth.AspNetCore package) are now used for claim and cookie setup. Authentication.md does not mention the shared library claim types (zb:username, zb:displayname) or that cookie hardening defaults come from ZbCookieDefaults.Apply. CLAIM_TYPE / behavior-rule VERDICT / gap EVIDENCE / src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs:111115, src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs:66. CODE_AREA / auth.cookie SEVERITY / low PROPOSED_FIX / Add a brief note in dashboard auth documentation about ZbClaimTypes (zb:username, zb:displayname, zb:name, zb:role) and ZbCookieDefaults.Apply providing cookie security defaults.