using System.Security.Cryptography;
using System.Text;
using GB5Shared.DTO.Framework.Enum;
using GB5Shared.DTO.Framework.ResponseStandard;
using GB5Shared.Vault;
using Newtonsoft.Json;
namespace FrameworkSL.Middleware
{
///
/// Gates every FrameworkSL/Endpoints/KeyCloak/* admin-API endpoint (CreateRealm, CreateClient,
/// CreateUser, GetMasterAccessToken, ...) — found this session all 15 were AllowAnonymous()
/// with zero authentication of any kind: anyone who could reach this host (confirmed
/// internet-reachable, no gateway, earlier this session) could create/delete Keycloak realms,
/// clients, and users, or retrieve a live Keycloak admin bearer token via GetMasterAccessToken.
///
/// Same idiom as EAIAdminApiKeyAuthMiddleware/KmsApiKeyAuthMiddleware/PartnerSL's
/// ApiKeyAuthMiddleware: X-Api-Key header, Vault-backed shared secret, constant-time SHA-256
/// comparison, fail CLOSED on any Vault outage. Gates the whole "/Keycloak" prefix (not an
/// exact-path allowlist like those examples) — every endpoint under it is part of the same
/// single admin surface, all equally sensitive (GetMasterAccessToken alone would leak a live
/// admin token to anyone who could reach it).
///
/// This key is GB5's OWN internal M2M shared secret protecting access to this admin-API
/// wrapper — distinct from Keycloak's own admin-cli credential at
/// "keycloak/admin-username"/"keycloak/admin-password" (KeyCloakDAL.GetMasterAccessToken),
/// which this middleware never touches.
///
/// No Login header is synthesized here (unlike EAIAdminApiKeyAuthMiddleware) — none of the
/// Keycloak endpoints' request records declare a Login property, so BaseEndPoint's
/// reflection-based lookup already returns null safely; nothing downstream expects one.
///
/// Registration (FrameworkSL/Program.cs): app.UseWhen(path starts with "/Keycloak", ...) —
/// same convention as PlatformHost's own Partner/KMS/EAIAdmin ApiKeyAuthMiddleware blocks.
///
public sealed class KeycloakApiKeyAuthMiddleware
{
private const string ApiKeyHeader = "X-Api-Key";
private const string VaultKeyPath = "keycloak-admin-api/shared-secret";
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public KeycloakApiKeyAuthMiddleware(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("KeycloakApiKeyAuthMiddleware: 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, "KeycloakApiKeyAuthMiddleware: Vault lookup failed — rejecting request");
await WriteUnauthorized(context, "Authentication temporarily unavailable. Please retry.");
return;
}
if (string.IsNullOrWhiteSpace(expectedKey) || !ConstantTimeEquals(rawKey, expectedKey))
{
_logger.LogWarning(
"KeycloakApiKeyAuthMiddleware: invalid API key (hint: last-4={Hint}) on {Path}",
rawKey.Length >= 4 ? rawKey[^4..] : "???", context.Request.Path);
await WriteUnauthorized(context, "Invalid API key.");
return;
}
_logger.LogInformation("KeycloakApiKeyAuthMiddleware: 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