31 KiB
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 253–271
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 53–68
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:5–8 — 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 ApiKeyVerificationResult → ApiKeyVerification (the type returned by IApiKeyVerifier.VerifyAsync in the interceptor).
DOC / Authentication.md / LINES 72–98
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 108–122
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 126–133
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:69–74 — 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 134–153
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 156–164
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 165–183
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 171–179 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 187–208
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:5–13 — 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 170–177 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 229–248
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 107–113
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:23–28 — 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 260–270
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:18–31 — 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 63–66
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 181–182
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:35–48 — 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 113–136
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 261–269
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: UseTls → Transport (enum), AllowInsecureLdap → AllowInsecure.
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:5–13 — 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 70–74
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:63–64 confirms "GwAdmin": "Administrator", "GwReader": "Viewer".
CODE_AREA / auth.roles
SEVERITY / low
PROPOSED_FIX / flag only
DOC / glauth.md / LINES 21–26
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 1–30
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 189–208
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 220–225
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 94–116
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:13–29 — 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 205–215
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:91–97, 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:85–94, 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:111–115, 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.