using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using System.Text;
namespace FrameworkSL.Middleware
{
///
/// Validates inbound webhook payloads on EIP channel endpoints before
/// FastEndpoints deserializes the body.
///
/// How it works:
/// 1. Enables request body buffering (so the body can be read twice).
/// 2. For paths under /EIPConversation/, reads the raw body and the
/// channel-specific signature header.
/// 3. Computes HMAC-SHA256 with the per-channel secret from appsettings.
/// 4. Returns HTTP 401 if the signature is absent (when strict mode is on)
/// or if the signature is present but invalid.
/// 5. Rewinds the body so FastEndpoints can read it normally.
///
/// Configuration (appsettings.json):
/// "EIP": {
/// "Enable": "Y",
/// "WebhookStrictValidation": false, // false = warn-only; true = reject on missing signature
/// "WebhookSecrets": {
/// "WhatsApp": "your-meta-app-secret", // sha256 of body; X-Hub-Signature-256 header
/// "Teams": "your-teams-hmac-key", // Authorization header (Bot Framework)
/// "Telegram": "your-telegram-token" // X-Telegram-Bot-Api-Secret-Token header
/// }
/// }
///
/// To enable: add app.UseMiddleware<EIPWebhookSignatureMiddleware>();
/// in Program.cs BEFORE app.UseFastEndpoints().
///
public class EIPWebhookSignatureMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IConfiguration _config;
private static readonly HashSet _eipPaths = new(StringComparer.OrdinalIgnoreCase)
{
"/EIPConversation/RequestEIPConversation",
"/EIPConversation/Receive"
};
public EIPWebhookSignatureMiddleware(
RequestDelegate next,
ILogger logger,
IConfiguration config)
{
_next = next;
_logger = logger;
_config = config;
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? string.Empty;
// Only inspect EIP webhook endpoints
if (!_eipPaths.Contains(path))
{
await _next(context);
return;
}
// Enable buffering so the body can be read here AND again by FastEndpoints
context.Request.EnableBuffering();
try
{
var rawBody = await ReadBodyAsync(context.Request);
var strictMode = _config.GetValue("EIP:WebhookStrictValidation");
var channelHeader = context.Request.Headers["X-EIP-Channel"].FirstOrDefault() ?? string.Empty;
// Detect channel from header (WhatsApp sends X-Hub-Signature-256;
// set X-EIP-Channel=WhatsApp in your Celitix/Meta webhook config)
var valid = channelHeader.Equals("WhatsApp", StringComparison.OrdinalIgnoreCase)
? ValidateWhatsApp(context.Request, rawBody, strictMode)
: ValidateGeneric(context.Request, rawBody, channelHeader, strictMode);
if (!valid)
{
_logger.LogWarning(
"EIP webhook signature validation FAILED | Path={Path} | Channel={Channel} | IP={IP}",
path, channelHeader, context.Connection.RemoteIpAddress);
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Webhook signature invalid.");
return;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "EIP webhook signature middleware error | Path={Path}", path);
// Don't block on middleware error — let the endpoint handle the request
}
finally
{
// Rewind so FastEndpoints can read the body
context.Request.Body.Seek(0, SeekOrigin.Begin);
}
await _next(context);
}
private bool ValidateWhatsApp(HttpRequest request, string rawBody, bool strictMode)
{
var secret = _config["EIP:WebhookSecrets:WhatsApp"];
var signatureHeader = request.Headers["X-Hub-Signature-256"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(signatureHeader))
{
_logger.LogWarning("WhatsApp webhook missing X-Hub-Signature-256 header");
return !strictMode; // strict=false → warn-only; strict=true → reject
}
if (string.IsNullOrWhiteSpace(secret))
{
_logger.LogWarning("EIP:WebhookSecrets:WhatsApp not configured — skipping signature check");
return true;
}
// Format: "sha256="
var expected = $"sha256={ComputeHmacSha256Hex(rawBody, secret)}";
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader));
}
private bool ValidateGeneric(HttpRequest request, string rawBody, string channel, bool strictMode)
{
var secret = _config[$"EIP:WebhookSecrets:{channel}"];
if (string.IsNullOrWhiteSpace(secret)) return true; // no secret configured → skip
// Generic: X-Webhook-Signature or X-Hub-Signature-256
var sigHeader = request.Headers["X-Hub-Signature-256"].FirstOrDefault()
?? request.Headers["X-Webhook-Signature"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(sigHeader))
return !strictMode;
var expected = ComputeHmacSha256Hex(rawBody, secret);
var provided = sigHeader.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase)
? sigHeader[7..]
: sigHeader;
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(provided));
}
private static async Task ReadBodyAsync(HttpRequest request)
{
request.Body.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
return await reader.ReadToEndAsync();
}
private static string ComputeHmacSha256Hex(string body, string secret)
{
var key = Encoding.UTF8.GetBytes(secret);
var data = Encoding.UTF8.GetBytes(body);
var hash = HMACSHA256.HashData(key, data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
}