using System.Security.Cryptography; using System.Text; using System.Text.Json; using FLSBLL.Session; using FLSDAL.CustomCode.Registration; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace FLSBLL.Handlers; internal record WebhookPayload( string ProviderCode, string ExternalReference, int RespondentRefId, string? ResponseData); public class FlsWebhookHandler { private readonly IFlsRegistrationDAL _regDal; private readonly IFlsSessionBLL _sessionBll; private readonly ILogger _logger; public FlsWebhookHandler( IFlsRegistrationDAL regDal, IFlsSessionBLL sessionBll, ILogger logger) { _regDal = regDal; _sessionBll = sessionBll; _logger = logger; } // Plan §4.6: HMAC-SHA256 verification per provider before processing any payload public async Task<(bool Success, string Message)> HandleAsync( string providerCode, string rawBody, string? hmacHeader, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("fls-webhook-received", new { providerCode }); // Load webhook secret for this provider from registration config var reg = await _regDal.GetByWebhookProviderAsync(providerCode, login, ct).ConfigureAwait(false); if (reg is null) return (false, $"No registration found for webhook provider '{providerCode}'."); if (!string.IsNullOrEmpty(reg.WebhookSecret)) { if (string.IsNullOrEmpty(hmacHeader)) { _logger.LogWarning("Webhook HMAC header missing for provider {Provider}", providerCode); return (false, "Missing HMAC signature."); } if (!VerifyHmac(rawBody, hmacHeader, reg.WebhookSecret)) { GB5Trace.MarkFailed("fls-webhook-hmac-invalid", new Exception("HMAC mismatch")); _logger.LogWarning("Webhook HMAC mismatch for provider {Provider}", providerCode); return (false, "Invalid HMAC signature."); } } GB5Trace.Step("fls-webhook-process", new { providerCode, reg.FlsRegistrationId }); var payload = JsonSerializer.Deserialize(rawBody); if (payload is null) return (false, "Invalid payload format."); await _sessionBll.MarkExternalSubmitAsync( flsRespondentId: payload.RespondentRefId, sourceReference: payload.ExternalReference, login, ct).ConfigureAwait(false); return (true, "Webhook processed."); } catch (Exception ex) { GB5Trace.MarkFailed("fls-webhook-failed", ex); _logger.LogError(ex, "WebhookHandler failed for provider {Provider}", providerCode); throw; } } // HMAC-SHA256 verification — constant-time comparison to prevent timing attacks private static bool VerifyHmac(string body, string incomingSignature, string secret) { using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var computed = hmac.ComputeHash(Encoding.UTF8.GetBytes(body)); var computedHex = Convert.ToHexString(computed).ToLowerInvariant(); // Normalize: strip prefix like "sha256=" if present var normalized = incomingSignature.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase) ? incomingSignature[7..] : incomingSignature; return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(computedHex), Encoding.UTF8.GetBytes(normalized.ToLowerInvariant())); } }