using System.Security.Cryptography;
using System.Text;
using GB5Shared.DTO.Framework.Enum;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Framework.ResponseStandard;
using GB5Shared.Vault;
using Newtonsoft.Json;
namespace KmsSL.Middleware
{
///
/// Authenticates the machine-to-machine calls gbEAI's kms_discovery_adapter.py makes directly
/// into /KmDiscovery/SaveCandidate and /KmDiscovery/UpdateJobStatus via the X-Api-Key header —
/// unlike every other KMS endpoint (browser-session-driven, AllowAnonymous() + a real Login
/// header decoded server-side), these two have no HTTP session to inherit tenant identity
/// from, so they need their own explicit check. Same idiom as
/// PartnerSL.Middleware.ApiKeyAuthMiddleware (the only established precedent for this in the
/// repo — no IPreProcessor usage exists anywhere), simplified for a single external caller
/// (gbEAI) instead of a partner registry: one shared secret in Vault, not a per-caller DB table.
///
/// Flow per request:
/// 1. Path not in the exact allowlist below → skip entirely (never touches the
/// browser-driven KmDiscovery endpoints or any other KMS route).
/// 2. No X-Api-Key header → 401 (these two paths REQUIRE it; there is no session fallback).
/// 3. Resolve the expected key from Vault (kms/gbeai-api-key) and compare via constant-time
/// SHA-256 hash comparison.
/// 4. Vault unreachable / key missing / mismatch → 401 (fail CLOSED, matching Partner's own
/// documented discipline — a transient outage must reject the request, not silently let
/// it through).
/// 5. Synthesize a PLACEHOLDER LoginDTO (ClientId = -1, UserId = 0) and inject it as the
/// Login header — this only satisfies FastEndpoints' [FromHeader] string Login binding.
/// The real tenant identity for this call is the DTO's own TenantId field (already on
/// the wire in KmCandidateDTO/KmExtractionJobDTO) — KmDiscoveryBLL.SaveCandidate/
/// UpdateJobStatus rebuild a real, DB-routable LoginDTO from that field, matching the
/// established `new LoginDTO { ClientId = tenantId, UserId = -1 }` idiom already used by
/// EntitlementLoginFactory/FlsReminderJobHandler/ComplianceSL's AlertCheckSubscriber for
/// exactly this "no HTTP session, only a tenant id" situation.
///
/// Registration (PlatformHost/Program.cs), exact-path allowlist via app.UseWhen — see that
/// file's existing Partner ApiKeyAuthMiddleware UseWhen block for the sibling pattern.
///
public sealed class KmsApiKeyAuthMiddleware
{
private const string ApiKeyHeader = "X-Api-Key";
private const string VaultKeyPath = "kms/gbeai-api-key";
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public KmsApiKeyAuthMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, IVaultService vaultService)
{
if (!context.Request.Headers.TryGetValue(ApiKeyHeader, out var rawKeyValues)
|| string.IsNullOrWhiteSpace(rawKeyValues.FirstOrDefault()))
{
_logger.LogWarning("KmsApiKeyAuthMiddleware: missing X-Api-Key header on {Path}", context.Request.Path);
await WriteUnauthorized(context, "X-Api-Key header is required.");
return;
}
var rawKey = rawKeyValues.First()!.Trim();
string? expectedKey;
try
{
expectedKey = await vaultService.GetSecretAsync(VaultKeyPath, context.RequestAborted)
.ConfigureAwait(false);
}
catch (Exception ex)
{
// Fail CLOSED — a Vault outage must reject the request, never silently authenticate.
_logger.LogError(ex, "KmsApiKeyAuthMiddleware: Vault lookup failed — rejecting request");
await WriteUnauthorized(context, "Authentication temporarily unavailable. Please retry.");
return;
}
if (string.IsNullOrWhiteSpace(expectedKey) || !ConstantTimeEquals(rawKey, expectedKey))
{
_logger.LogWarning(
"KmsApiKeyAuthMiddleware: invalid API key (hint: last-4={Hint})",
rawKey.Length >= 4 ? rawKey[^4..] : "???");
await WriteUnauthorized(context, "Invalid API key.");
return;
}
// Placeholder only — see class doc comment. Real tenant identity comes from the
// request body's own TenantId field, resolved inside the BLL.
var login = new LoginDTO { ClientId = -1, UserId = 0 };
context.Request.Headers["Login"] = JsonConvert.SerializeObject(login);
_logger.LogInformation("KmsApiKeyAuthMiddleware: authenticated M2M call | Path={Path}", context.Request.Path);
await _next(context);
}
private static bool ConstantTimeEquals(string rawKey, string expectedKey)
{
var rawHash = SHA256.HashData(Encoding.UTF8.GetBytes(rawKey));
var expectedHash = SHA256.HashData(Encoding.UTF8.GetBytes(expectedKey));
return CryptographicOperations.FixedTimeEquals(rawHash, expectedHash);
}
private static async Task WriteUnauthorized(HttpContext context, string message)
{
var response = new ResponseStandardDTO