Round #285
08 May 2026, 12:31· csharp· openai/gpt-oss-120b:free· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 5 / 4 / 1
- Journey weight
- 612
- Lightshift ΔL
- -0.50
Prompt
Implement a webhook delivery endpoint with HMAC signature verification
Zero-point index (what the engine surfaced)
ZPI-ZPI-0003 | 2026-05-08T12:31:54Z
Implement a webhook delivery endpoint with HMAC signature verification
ZERO POINT — 5 entries (structural invariants)
● hsl(27.69, 100%, 50%) async [feature]
● hsl(145.64, 100%, 27%) Task of T [feature]
● hsl(7.18, 100%, 38%) class [feature]
● hsl(0.00, 100%, 38%) public [feature]
● hsl(84.10, 100%, 38%) namespace [feature]
UNOBSERVED — 4 entries (implied truths)
○ hsl(9.23, 100%, 50%) interface [feature]
○ hsl(77.54, 100%, 36%) Routing middleware [function]
○ hsl(83.08, 100%, 42%) ILogger of T injection [function]
○ hsl(31.38, 100%, 42%) Scoped registration [function]
OBSERVED — 1 entries (explicit in request)
◉ hsl(98.46, 100%, 28%) with [feature]
COMPLEXITY │ 10 entries │ spread: 89° │ band: LOW
Baseline response (no ZPI)
Below is a **complete, production‑ready** example of a webhook‑delivery endpoint that
1. **Receives** a POST request containing a JSON payload.
2. **Validates** an HMAC‑SHA256 signature that the sender (the webhook provider) adds in an HTTP header.
3. **Returns** the appropriate HTTP status codes (`200 OK` on success, `401 Unauthorized` on signature failure, `400 Bad Request` on malformed payload, etc.).
4. **Is easy to test** with tools like `curl` or Postman.
The implementation is shown for three common ASP.NET Core hosting models:
| Scenario | Where the code lives | Why you might choose it |
|----------|----------------------|------------------------|
| **Minimal API** (ASP.NET Core 6+) | `Program.cs` | Small services, micro‑functions, or when you want the least ceremony. |
| **Controller‑based API** (ASP.NET Core MVC) | `Controllers/WebhookController.cs` | Larger apps that already use MVC, attribute routing, filters, etc. |
| **Middleware** (global verification) | `Middleware/HmacVerificationMiddleware.cs` | When *all* incoming requests to a path (or the whole app) must be verified before they reach any controller. |
---
## 1. Shared secret configuration
All three approaches read a **shared secret** (the key used to compute the HMAC) from configuration – e.g. `appsettings.json`, environment variables, Azure Key Vault, etc.
```json
// appsettings.json
{
"Webhook": {
"Secret": "YOUR_SUPER_SECRET_KEY_BASE64" // store as base64 or plain text (but protect!)
}
}
```
> **Security tip** – Never commit the secret to source control. Use environment variables or a secret manager in production.
---
## 2. Helper class – HMAC verification logic
Put the verification code in a reusable static helper (or a service if you prefer DI).
It does **constant‑time** comparison to avoid timing attacks.
```csharp
// Services/HmacValidator.cs
using System;
using System.Security.Cryptography;
using System.Text;
namespace MyWebhookApp.Services
{
public static class HmacValidator
{
/// <summary>
/// Validates that the supplied signature matches the HMAC‑SHA256 of the request body.
/// </summary>
/// <param name="secret">The shared secret (raw bytes).</param>
/// <param name="payload">The raw request body (bytes).</param>
/// <param name="providedSignature">Signature string from the header (e.g. "sha256=abcd...").</param>
/// <returns>True if the signature is valid; otherwise false.</returns>
public static bool IsValid(byte[] secret, byte[] payload, string? providedSignature)
{
if (string.IsNullOrWhiteSpace(providedSignature))
return false;
// Expected format: "sha256=hexstring"
const string prefix = "sha256=";
if (!providedSignature.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
return false;
var hexSignature = providedSignature[prefix.Length..];
if (hexSignature.Length % 2 != 0)
return false; // malformed hex
byte[] signatureBytes;
try
{
signatureBytes = Convert.FromHexString(hexSignature);
}
catch (FormatException)
{
return false;
}
// Compute HMAC‑SHA256 of the payload
using var hmac = new HMACSHA256(secret);
var computedHash = hmac.ComputeHash(payload);
// Constant‑time comparison
return CryptographicOperations.FixedTimeEquals(computedHash, signatureBytes);
}
/// <summary>
/// Helper to decode the secret from configuration (base64 or plain text).
/// </summary>
public static byte[] DecodeSecret(string secretFromConfig)
{
// If the secret looks like base64, decode it; otherwise treat it as UTF‑8 bytes.
try
{
return Convert.FromBase64String(secretFromConfig);
}
catch (FormatException)
{
return Encoding.UTF8.GetBytes(secretFromConfig);
}
}
}
}
```
---
## 3. Minimal API implementation (ASP.NET Core 6+)
```csharp
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MyWebhookApp.Services;
var builder = WebApplication.CreateBuilder(args);
// Bind the secret once at startup
var webhookSecret = builder.Configuration.GetSection("Webhook:Secret").Value
?? throw new InvalidOperationException("Webhook secret not configured");
var secretBytes = HmacValidator.DecodeSecret(webhookSecret);
var app = builder.Build();
app.MapPost("/webhook", async (HttpRequest request) =>
{
// 1️⃣ Read the raw body (do NOT use model binding before verification)
using var ms = new MemoryStream();
await request.Body.CopyToAsync(ms);
var payloadBytes = ms.ToArray();
// 2️⃣ Grab the signature header (the name is provider‑specific; here we use "X-Hub-Signature-256")
var signatureHeader = request.Headers["X-Hub-Signature-256"].FirstOrDefault();
// 3️⃣ Verify
if (!HmacValidator.IsValid(secretBytes, payloadBytes, signatureHeader))
{
return Results.Unauthorized(); // 401
}
// 4️⃣ At this point the payload is trusted – deserialize it
// (replace `MyWebhookPayload` with your own POCO)
var payload = System.Text.Json.JsonSerializer.Deserialize<MyWebhookPayload>(payloadBytes);
if (payload is null)
{
return Results.BadRequest("Invalid JSON payload");
}
// 5️⃣ Do whatever you need with the payload
// e.g. enqueue a background job, update DB, etc.
// For demo purposes we just echo it back:
return Results.Ok(new { received = true, payload });
})
.Accepts<MyWebhookPayload>("application/json")
.Produces(200)
.Produces(401)
.Produces(400);
app.Run();
// ---------------------------------------------------
// Example POCO for the payload (adjust fields as needed)
public record MyWebhookPayload
{
public string Event { get; init; } = default!;
public DateTime Timestamp { get; init; }
public JsonElement Data { get; init; } // use JsonElement for dynamic payloads
}
```
### How to test (curl)
```bash
# Sample payload
payload='{"event":"order.created","timestamp":"2024-05-01T12:34:56Z","data":{"orderId":123}}'
# Compute HMAC‑SHA256 using the same secret (replace with your secret)
secret='YOUR_SUPER_SECRET_KEY_BASE64' # same as in appsettings
# If secret is plain text, just echo -n "$secret"
key=$(echo -n "$secret" | base64 -d 2>/dev/null || echo -n "$secret")
signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$key" -binary | xxd -p -c 256)
header="sha256=$signature"
curl -X POST http://localhost:5000/webhook \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $header" \
-d "$payload"
```
You should receive a JSON response with `"received": true`.
---
## 4. Controller‑based implementation (ASP.NET Core MVC)
```csharp
// Controllers/WebhookController.cs
using Microsoft.AspNetCore.Mvc;
using MyWebhookApp.Services;
namespace MyWebhookApp.Controllers;
[ApiController]
[Route("[controller]")] // POST /webhook
public class WebhookController : ControllerBase
{
private readonly byte[] _secret;
public WebhookController(IConfiguration configuration)
{
var secretString = configuration.GetValue<string>("Webhook:Secret")
?? throw new InvalidOperationException("Webhook secret missing");
_secret = HmacValidator.DecodeSecret(secretString);
}
[HttpPost]
public async Task<IActionResult> Receive()
{
// 1️⃣ Read raw body
using var ms = new MemoryStream();
await Request.Body.CopyToAsync(ms);
var payloadBytes = ms.ToArray();
// 2️⃣ Header (adjust name if your provider uses a different one)
var signatureHeader = Request.Headers["X-Hub-Signature-256"].FirstOrDefault();
// 3️⃣ Verify
if (!HmacValidator.IsValid(_secret, payloadBytes, signatureHeader))
{
return Unauthorized(); // 401
}
// 4️⃣ Deserialize (you can also use System.Text.Json directly)
MyWebhookPayload? payload;
try
{
payload = System.Text.Json.JsonSerializer.Deserialize<MyWebhookPayload>(payloadBytes);
}
catch (System.Text.Json.JsonException)
{
return BadRequest("Invalid JSON");
}
if (payload is null)
return BadRequest("Empty payload");
// 5️⃣ Process payload (business logic)
// Example: log it
// _logger.LogInformation("Webhook received: {Event}", payload.Event);
return Ok(new { status = "ok" });
}
}
```
Add the controller to the pipeline (default in ASP.NET Core MVC templates):
```csharp
// Program.cs (or Startup.cs for older versions)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // <-- make sure this is called
var app = builder.Build();
app.MapControllers(); // <-- route to controllers
app.Run();
```
---
## 5. Global middleware approach
If **every** request to a certain path (or the whole API) must be signed, you can place the verification in a middleware component. This keeps controllers clean.
```csharp
// Middleware/HmacVerificationMiddleware.cs
using Microsoft.AspNetCore.Http;
using MyWebhookApp.Services;
using System.Threading.Tasks;
namespace MyWebhookApp.Middleware;
public class HmacVerificationMiddleware
{
private readonly RequestDelegate _next;
private readonly byte[] _secret;
private readonly string _signatureHeaderName;
public HmacVerificationMiddleware(RequestDelegate next, IConfiguration config, string signatureHeaderName = "X-Hub-Signature-256")
{
_next = next;
var secretString = config.GetValue<string>("Webhook:Secret")
?? throw new InvalidOperationException("Webhook secret missing");
_secret = HmacValidator.DecodeSecret(secretString);
_signatureHeaderName = signatureHeaderName;
}
public async Task InvokeAsync(HttpContext context)
{
// Only verify POST/PUT/PATCH (adjust as needed)
if (!HttpMethods.IsPost(context.Request.Method) &&
!HttpMethods.IsPut(context.Request.Method) &&
!HttpMethods.IsPatch(context.Request.Method))
{
await _next(context);
return;
}
// Buffer the request body so we can read it multiple times
context.Request.EnableBuffering();
using var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms);
var payload = ms.ToArray();
// Reset the stream position for downstream components
context.Request.Body.Position = 0;
var signatureHeader = context.Request.Headers[_signatureHeaderName].FirstOrDefault();
if (!HmacValidator.IsValid(_secret, payload, signatureHeader))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Invalid HMAC signature");
return;
}
// Signature ok – continue down the pipeline
await _next(context);
}
}
```
Register the middleware (order matters – it must run **before** MVC):
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
// Apply only to /webhook/* paths (optional)
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/webhook"),
appBuilder => appBuilder.UseMiddleware<MyWebhookApp.Middleware.HmacVerificationMiddleware>()
);
app.MapControllers();
app.Run();
```
Now your controller can assume the request is already verified and can use normal model binding:
```csharp
[HttpPost]
public IActionResult Receive([FromBody] MyWebhookPayload payload)
{
// No signature checks here – middleware already did it
// Process payload...
return Ok();
}
```
---
## 6. Full project structure (suggested)
```
MyWebhookApp/
│
├─ Controllers/
│ └─ WebhookController.cs
│
├─ Middleware/
│ └─ HmacVerificationMiddleware.cs
│
├─ Services/
│ └─ HmacValidator.cs
│
├─ Models/
│ └─ MyWebhookPayload.cs
│
├─ appsettings.json
├─ Program.cs
└─ MyWebhookApp.csproj
```
---
## 7. Testing strategies
| Test type | How to perform |
|-----------|----------------|
| **Unit test** for `HmacValidator.IsValid` | Use xUnit/NUnit, feed known payload + secret + expected signature, assert true/false. |
| **Integration test** (ASP.NET Core TestServer) | Spin up `WebApplicationFactory`, send a request with a valid/invalid signature, assert status codes. |
| **Load test** | Use `k6` or `ab` to hit the endpoint with a realistic payload size; ensure the HMAC verification does not become a bottleneck. |
| **Security test** | Verify that timing differences between valid/invalid signatures are negligible (the `FixedTimeEquals` call guarantees this). |
---
## 8. Common pitfalls & how we avoided them
| Pitfall | What we did |
|---------|--------------|
| **Reading the body after model binding** – the stream is already consumed. | We read the raw body **first**, then reset `Request.Body.Position = 0` (or use `EnableBuffering`) before any further processing. |
| **Using string comparison for signatures** – vulnerable to timing attacks. | Used `CryptographicOperations.FixedTimeEquals`. |
| **Hard‑coding the secret** – leaks in source control. | Secret is read from configuration; you can replace it with Azure Key Vault, AWS Secrets Manager, etc. |
| **Assuming the header is always present** – leads to `NullReferenceException`. | Guarded with null checks and returned `401` when missing/invalid. |
| **Incorrect header format** – some providers send `sha1=` or no prefix. | The validator expects the `sha256=` prefix; you can easily adapt the code for other algorithms. |
| **Base64 vs plain‑text secret** – mismatched encoding causes false failures. | `DecodeSecret` tries base64 first, then falls back to UTF‑8 bytes. |
---
## 9. Quick “copy‑paste” starter (Minimal API)
If you just need a single file to drop into a new project:
```csharp
// Program.cs
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
var secretString = builder.Configuration.GetValue<string>("Webhook:Secret")
?? throw new InvalidOperationException("Webhook secret missing");
byte[] secret = DecodeSecret(secretString);
var app = builder.Build();
app.MapPost("/webhook", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
var payload = ms.ToArray();
var signature = req.Headers["X-Hub-Signature-256"].FirstOrDefault();
if (!IsValid(secret, payload, signature))
return Results.Unauthorized();
// trusted payload – deserialize as needed
var json = Encoding.UTF8.GetString(payload);
return Results.Ok(new { ok = true, received = json });
});
app.Run();
static byte[] DecodeSecret(string s) =>
Convert.TryFromBase64String(s, out var bytes) ? bytes : Encoding.UTF8.GetBytes(s);
static bool IsValid(byte[] secret, byte[] payload, string? header)
{
if (string.IsNullOrEmpty(header) || !header.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase))
return false;
var hex = header["sha256=".Length..];
if (hex.Length % 2 != 0) return false;
byte[] provided;
try { provided = Convert.FromHexString(hex); }
catch { return false; }
using var hmac = new HMACSHA256(secret);
var computed = hmac.ComputeHash(payload);
return CryptographicOperations.FixedTimeEquals(computed, provided);
}
```
Add the secret to `appsettings.json` (or an environment variable) and you’re ready to go.
---
### 🎉 You now have a fully functional, secure webhook endpoint in C# for ASP.NET Core, with three interchangeable patterns, a reusable HMAC validator, and guidance on testing and deployment. Happy coding!
Enhanced response (with ZPI)
**Solution Overview**
We will create a small ASP.NET Core Web API that exposes a single **POST** endpoint
`/api/webhook` which can be used by any external system that sends JSON payloads
with an HMAC‑SHA256 signature.
* The signature is expected in the request header `X‑Signature`.
* The secret key used to compute the HMAC is stored in configuration (`appsettings.json`) and injected via a scoped service (`IWebhookValidator`).
* The validator recomputes the HMAC over the raw request body and compares it with the header value using a time‑constant comparison.
* If the signature is valid the payload is passed to a downstream handler (`IWebhookProcessor`).
* All services are registered as **scoped** and `ILogger<T>` is injected wherever logging is required.
The code below is a complete, production‑ready example that can be dropped into a new ASP.NET Core project (targeting .NET 8, but works on .NET 6+).
---
## 1. Domain model / contracts
```csharp
// Represents the JSON payload that the webhook sends.
// Adjust the properties to match the actual contract you expect.
public sealed record WebhookPayload
{
public string Event { get; init; } = default!;
public DateTimeOffset Timestamp { get; init; }
public JsonElement Data { get; init; } // keep it flexible
}
```
---
## 2. HMAC verification service
```csharp
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
public interface IWebhookValidator
{
/// <summary>
/// Validates the HMAC signature of a request body.
/// </summary>
/// <param name="body">Raw request body (UTF‑8 bytes).</param>
/// <param name="signatureHeader">Value of the X‑Signature header.</param>
/// <returns>True if the signature matches the secret; otherwise false.</returns>
Task<bool> IsValidAsync(byte[] body, string? signatureHeader);
}
public sealed class WebhookValidator : IWebhookValidator
{
private readonly ILogger<WebhookValidator> _logger;
private readonly string _secret;
public WebhookValidator(IOptions<WebhookOptions> options,
ILogger<WebhookValidator> logger)
{
_logger = logger;
_secret = options.Value.Secret ?? throw new InvalidOperationException("Webhook secret is not configured.");
}
public Task<bool> IsValidAsync(byte[] body, string? signatureHeader)
{
if (string.IsNullOrWhiteSpace(signatureHeader))
{
_logger.LogWarning("Missing signature header.");
return Task.FromResult(false);
}
// Expected format: "sha256=abcdef..."
const string prefix = "sha256=";
if (!signatureHeader.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Signature header does not start with expected prefix.");
return Task.FromResult(false);
}
var providedSignature = signatureHeader[prefix.Length..];
// Compute HMAC‑SHA256 of the request body using the secret.
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secret));
var computedHash = hmac.ComputeHash(body);
var computedSignature = Convert.ToHexString(computedHash).ToLowerInvariant();
// Time‑constant comparison to avoid timing attacks.
var isValid = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedSignature),
Encoding.UTF8.GetBytes(providedSignature));
if (!isValid)
{
_logger.LogWarning("Invalid HMAC signature. Expected {Expected}, received {Received}.",
computedSignature, providedSignature);
}
return Task.FromResult(isValid);
}
}
```
**Configuration POCO**
```csharp
public sealed class WebhookOptions
{
public string? Secret { get; set; }
}
```
Add the section to *appsettings.json*:
```json
{
"Webhook": {
"Secret": "YOUR_SUPER_SECRET_KEY"
}
}
```
---
## 3. Payload processing service (business logic)
```csharp
public interface IWebhookProcessor
{
Task ProcessAsync(WebhookPayload payload, CancellationToken ct = default);
}
public sealed class WebhookProcessor : IWebhookProcessor
{
private readonly ILogger<WebhookProcessor> _logger;
public WebhookProcessor(ILogger<WebhookProcessor> logger)
{
_logger = logger;
}
public Task ProcessAsync(WebhookPayload payload, CancellationToken ct = default)
{
// Example: just log the payload. Replace with real handling (store, trigger, etc.).
_logger.LogInformation("Received webhook event {Event} at {Timestamp}. Data: {Data}",
payload.Event, payload.Timestamp, payload.Data.GetRawText());
// Simulate async work.
return Task.CompletedTask;
}
}
```
---
## 4. The controller / endpoint
```csharp
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
[ApiController]
[Route("api/[controller]")]
public sealed class WebhookController : ControllerBase
{
private readonly IWebhookValidator _validator;
private readonly IWebhookProcessor _processor;
private readonly ILogger<WebhookController> _logger;
public WebhookController(IWebhookValidator validator,
IWebhookProcessor processor,
ILogger<WebhookController> logger)
{
_validator = validator;
_processor = processor;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> ReceiveAsync(CancellationToken ct)
{
// 1️⃣ Read raw body (must be done before model binding)
await using var ms = new MemoryStream();
await Request.Body.CopyToAsync(ms, ct);
var bodyBytes = ms.ToArray();
// 2️⃣ Verify HMAC
var signatureHeader = Request.Headers["X-Signature"].FirstOrDefault();
if (!await _validator.IsValidAsync(bodyBytes, signatureHeader))
{
_logger.LogWarning("Webhook request rejected due to invalid signature.");
return Unauthorized(); // 401
}
// 3️⃣ Deserialize payload (JSON)
WebhookPayload? payload;
try
{
payload = JsonSerializer.Deserialize<WebhookPayload>(bodyBytes, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize webhook payload.");
return BadRequest("Invalid JSON payload.");
}
if (payload is null)
{
_logger.LogWarning("Deserialized payload is null.");
return BadRequest("Empty payload.");
}
// 4️⃣ Process the payload
await _processor.ProcessAsync(payload, ct);
// 5️⃣ Respond with 202 Accepted (asynchronous processing)
return Accepted();
}
}
```
**Why we read the raw body manually**
Model binding would consume the request stream, making it impossible to compute the HMAC over the exact bytes that the sender signed. By copying the stream into a `MemoryStream` we keep the original bytes for verification and then deserialize from the same byte array.
---
## 5. DI registration (Program.cs)
```csharp
var builder = WebApplication.CreateBuilder(args);
// 1️⃣ Configuration
builder.Services.Configure<WebhookOptions>(builder.Configuration.GetSection("Webhook"));
// 2️⃣ Scoped services (as requested)
builder.Services.AddScoped<IWebhookValidator, WebhookValidator>();
builder.Services.AddScoped<IWebhookProcessor, WebhookProcessor>();
// 3️⃣ Controllers / minimal API
builder.Services.AddControllers();
// 4️⃣ Logging is already set up by default (ILogger<T> injection works out‑of‑the‑box)
// Build and run
var app = builder.Build();
app.MapControllers();
app.Run();
```
All services are **scoped**, meaning a new instance is created per HTTP request, which is ideal for request‑specific data (e.g., the secret is read once per request, and the logger is request‑scoped).
---
## 6. End‑to‑end test (optional)
You can test the endpoint with `curl`:
```bash
payload='{"event":"order.created","timestamp":"2024-05-08T12:34:56Z","data":{"orderId":123}}'
secret='YOUR_SUPER_SECRET_KEY'
# Compute HMAC
signature=$(printf "%s" "$payload" | openssl dgst -sha256 -hmac "$secret" -binary | xxd -p -c 256)
signature="sha256=$signature"
curl -X POST http://localhost:5000/api/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: $signature" \
-d "$payload"
```
If the secret matches, the response will be `202 Accepted` and the server logs will show the received event.
---
## 7. Summary of key points
| Concern | Implementation |
|---------|----------------|
| **Routing middleware** | `app.MapControllers()` + `[ApiController]` attribute routes the POST to `/api/webhook`. |
| **ILogger\<T> injection** | All services (`WebhookValidator`, `WebhookProcessor`, `WebhookController`) receive `ILogger<T>` via constructor injection. |
| **Scoped registration** | `AddScoped` is used for `IWebhookValidator` and `IWebhookProcessor`. |
| **HMAC verification** | `WebhookValidator` computes SHA‑256 HMAC over the raw request body and uses `CryptographicOperations.FixedTimeEquals` for a timing‑attack‑safe comparison. |
| **Error handling** | Missing/invalid signature → `401 Unauthorized`; JSON deserialization errors → `400 Bad Request`; successful processing → `202 Accepted`. |
| **Configuration** | Secret stored in `appsettings.json` under `Webhook:Secret` and bound to `WebhookOptions`. |
This completes a production‑grade webhook delivery endpoint with secure HMAC signature verification, proper logging, and scoped DI registration.
Grader chain
Each grader reads the same baseline + enhanced pair and casts an independent verdict. Disagreement is the framework's own signal — not noise to be averaged away.
V1claude-haiku-4-5-20251001
BetterV4qwen2.5-coder-7b-instruct
BetterComments
Our grader said what it said. What do you say? Comment as a guest below.
No comments yet. Be the first to say what you make of this round.