fix(security): validate the API-key pepper at start, finish JWT bearer, circuit-aware audit actor
- ApiKeyOptionsValidator (ValidateOnStart on any driver/admin node): a blank or placeholder Security:ApiKeys:Pepper fails host start instead of rejecting every key after a green boot (R03:2.3). - JWT is finished and opt-in by key (R03:2.4): with Security:Jwt:SigningKey set, a Bearer scheme (JwtBearerAuthenticationHandler over JwtTokenService.TryValidate / BuildValidationParameters, fail closed) accepts the tokens /auth/token mints; the default scheme is a cookie-or-bearer policy scheme and the fallback policy names it, so /api/* and every RequireAuthorization endpoint take a browser session OR a machine token. JwtOptions are validated at start (JwtOptionsValidator). Without a key, /auth/token is not mapped (404, not the former constructor 500) and no bearer scheme exists. A direct AddJwtBearer needs Microsoft.AspNetCore.Authentication.JwtBearer in Directory.Packages.props (not this lane's file) — the handler is a drop-in to swap. - AuthenticationStateAuditActorAccessor is the registered IAuditActorAccessor: the Blazor circuit's principal first, HttpContext as the fallback (R03:2.5). HttpAuditActorAccessor shares its resolver. - CookieAuthenticationStateProvider (unregistered, fire-and-forget ping loop) is deleted (R03:1.5/2.5). - /auth/login logs the LDAP exception at Warning before answering 503 (R03:2.7). - Admin operations reply with a fixed message + correlation id instead of ex.Message (R03:4.1). Refs #733 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using ZB.MOM.WW.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Fail-fast startup validator for <see cref="ApiKeyOptions"/> (R03:2.3). The pepper used to be
|
||||
/// checked only when the first key was verified — <c>ApiKeySecret.Verify</c> throws on an empty one —
|
||||
/// so a site node deployed without <c>Security__ApiKeys__Pepper</c> booted green, passed health,
|
||||
/// applied deployments and denied every machine client with the same opaque "Invalid API key" until
|
||||
/// someone read the log; a central node with a wrong pepper minted keys that could verify nowhere.
|
||||
/// Every other security section already fails host start on a bad value; this one now does too.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Registered for <c>hasDriver || hasAdmin</c>: a driver node needs the pepper to VERIFY keys, an admin
|
||||
/// node to MINT them. The minimum length is a floor against a placeholder (<c>"x"</c>) passing as
|
||||
/// configured, not a strength claim — the pepper is a server-side secret, not a password.
|
||||
/// </remarks>
|
||||
public sealed class ApiKeyOptionsValidator : OptionsValidatorBase<ApiKeyOptions>
|
||||
{
|
||||
/// <summary>The shortest pepper accepted (characters, after trimming).</summary>
|
||||
public const int MinPepperLength = 16;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Validate(ValidationBuilder builder, ApiKeyOptions options)
|
||||
{
|
||||
var pepper = options.Pepper?.Trim() ?? string.Empty;
|
||||
builder.RequireThat(pepper.Length > 0,
|
||||
$"{ApiKeyOptions.SectionName}:Pepper is required on any node with the driver or admin role — " +
|
||||
"supply it via Security__ApiKeys__Pepper or a ${secret:} reference. An empty pepper would reject " +
|
||||
"every API key (driver) or mint keys that can never verify (admin).");
|
||||
builder.RequireThat(pepper.Length == 0 || pepper.Length >= MinPepperLength,
|
||||
$"{ApiKeyOptions.SectionName}:Pepper must be at least {MinPepperLength} characters.");
|
||||
builder.RequireThat(!string.IsNullOrWhiteSpace(options.Username),
|
||||
$"{ApiKeyOptions.SectionName}:Username must not be blank.");
|
||||
builder.RequireThat(options.StoreTimeoutMs >= 0,
|
||||
$"{ApiKeyOptions.SectionName}:StoreTimeoutMs must be >= 0 (0 disables the deadline).");
|
||||
builder.RequireThat(options.StoreCacheSeconds >= 0,
|
||||
$"{ApiKeyOptions.SectionName}:StoreCacheSeconds must be >= 0 (0 disables the cache).");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user