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 EAIAdminSL.Middleware
{
///
/// Authenticates gbEAI's callback into POST /EAIAdmin/ResolveContext — the first
/// gbEAI-calls-GB5 direction (every other integration so far is GB5 calling gbEAI). Same
/// idiom as KmsSL.Middleware.KmsApiKeyAuthMiddleware (itself modeled on
/// PartnerSL.Middleware.ApiKeyAuthMiddleware): X-Api-Key header, Vault-backed shared secret,
/// constant-time SHA-256 comparison, fail CLOSED on any Vault outage.
///
/// Flow per request:
/// 1. Path not in the exact allowlist (/EAIAdmin/ResolveContext only) → skip entirely.
/// 2. No X-Api-Key header → 401.
/// 3. Resolve the expected key from Vault (eaiadmin/gbeai-api-key) and compare via
/// constant-time hash comparison.
/// 4. Vault unreachable / key missing / mismatch → 401 (fail CLOSED).
/// 5. Synthesize a PLACEHOLDER LoginDTO (ClientId = -1, UserId = 0) and inject it as the
/// Login header — only satisfies FastEndpoints' [FromHeader] string Login binding. The
/// real tenant identity comes from the request body's own TenantId field (already on the
/// wire in ResolveContextParameters) — EAIContextResolverBLL rebuilds a real,
/// DB-routable LoginDTO from that field, same `new LoginDTO { ClientId = tenantId,
/// UserId = -1 }` idiom already used by EntitlementLoginFactory/FlsReminderJobHandler/
/// ComplianceSL.AlertCheckSubscriber/KmDiscoveryBLL.EffectiveLogin.
///
/// Registration (PlatformHost/Program.cs), exact-path allowlist via app.UseWhen — see that
/// file's existing Partner/KMS ApiKeyAuthMiddleware UseWhen blocks for the sibling pattern.
///
public sealed class EAIAdminApiKeyAuthMiddleware
{
private const string ApiKeyHeader = "X-Api-Key";
private const string VaultKeyPath = "eaiadmin/gbeai-api-key";
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public EAIAdminApiKeyAuthMiddleware(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("EAIAdminApiKeyAuthMiddleware: 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, "EAIAdminApiKeyAuthMiddleware: Vault lookup failed — rejecting request");
await WriteUnauthorized(context, "Authentication temporarily unavailable. Please retry.");
return;
}
if (string.IsNullOrWhiteSpace(expectedKey) || !ConstantTimeEquals(rawKey, expectedKey))
{
_logger.LogWarning(
"EAIAdminApiKeyAuthMiddleware: 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("EAIAdminApiKeyAuthMiddleware: 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