fix(security): finish the bearer-token design and cache role mappings (review 2026-10-21 05/U1, 05/P1)
- ManagementAuthenticator accepts Authorization: Bearer <jwt> alongside Basic on /management, the audit REST endpoints and the debug-stream hub. Tokens are validated locally (HS256 only, alg=none rejected, issuer/audience, lifetime with 30 s skew); roles and site scope inside a valid token are authoritative for its lifetime, so a bearer call costs no LDAP bind and no role re-map. - /auth/token returns expires_in + management_bearer:true, carries username, display name, roles and permitted sites, and answers 404 under DisableLogin. - CLI exchanges credentials once per process (in-memory CliTokenStore), sends Bearer, re-exchanges once on 401 then falls back to Basic; older/login-disabled servers are Basic-only; bad credentials are not retried with Basic. Debug stream uses the token via AccessTokenProvider. SCADABRIDGE_AUTH_MODE=basic opts out. - StartupValidator fails a Central boot on a JwtSigningKey under 32 UTF-8 bytes; fixed the false 'fail-fast at construction' comment. Removed the dead RefreshToken/RecordActivity/ShouldRefresh/IsIdleTimedOut members and the inert JwtRefreshThresholdMinutes; JwtExpiryMinutes now documented as the bearer lifetime (validated > 0). - RoleMapper reads through a process-wide RoleMappingCache (RoleMappingCacheSeconds, default 30 s): one refresh loads the table plus every Deployer mapping's scope rules, Invalidate() drops it (generation-guarded against in-flight refreshes). - Component-Security.md / Component-CLI.md / CLI README describe the real model (cookie claims ticket, bearer for CLI) instead of a cookie-embedded JWT.
This commit is contained in:
@@ -85,6 +85,17 @@ public static class StartupValidator
|
||||
.Require("ScadaBridge:Security:JwtSigningKey",
|
||||
_ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Security")["JwtSigningKey"]),
|
||||
"required for Central")
|
||||
// The key signs the CLI bearer JWT (/auth/token → ManagementAuthenticator).
|
||||
// JwtTokenService is resolved lazily (first token request), so its own
|
||||
// constructor check cannot fail the boot — a 1-byte key used to boot clean
|
||||
// and mint trivially forgeable tokens (review 2026-10-21 05/U1). HMAC-SHA256
|
||||
// needs >= 256 bits; measured in UTF-8 bytes, exactly as the service keys the
|
||||
// HMAC. A missing key is reported by the rule above, not twice.
|
||||
.Require("ScadaBridge:Security:JwtSigningKey",
|
||||
value => string.IsNullOrEmpty(value)
|
||||
|| System.Text.Encoding.UTF8.GetByteCount(value) >= ZB.MOM.WW.ScadaBridge.Security.SecurityOptions.MinJwtSigningKeyBytes,
|
||||
$"must be at least {ZB.MOM.WW.ScadaBridge.Security.SecurityOptions.MinJwtSigningKeyBytes} bytes "
|
||||
+ "(256 bits) for HMAC-SHA256 — it signs the CLI bearer tokens, and a short key makes them forgeable")
|
||||
// The inbound API-key pepper
|
||||
// backs the peppered-HMAC secret compare in the shared
|
||||
// ZB.MOM.WW.Auth.ApiKeys verifier (wired by AddZbApiKeyAuth at the
|
||||
|
||||
Reference in New Issue
Block a user