Round #310
08 May 2026, 13:17· csharp· openai/gpt-oss-120b:free· r13-clean-hints
- Complexity
- band 0
- ZP / UO / OBS
- 5 / 4 / 1
- Journey weight
- 612
- Lightshift ΔL
- +0.17
Prompt
Implement a webhook delivery endpoint with HMAC signature verification
Zero-point index (what the engine surfaced)
ZPI-ZPI-0003 | 2026-05-08T13:17:53Z
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** the request with an **HMAC‑SHA256** signature that the sender (the webhook provider) adds to the `X‑Signature` header.
3. **Rejects** the request with `401 Unauthorized` if the signature is missing or does not match.
4. **Processes** the payload only after the signature is verified.
The example is written for **ASP.NET Core 8** (the current LTS version at the time of writing) and uses the built‑in dependency‑injection (DI) container, the `ILogger` abstraction, and a small helper service (`IWebhookSignatureValidator`) that encapsulates the HMAC logic.
You can copy‑paste the code into a fresh ASP.NET Core Web API project, adjust the secret key and the payload model, and you’ll have a fully functional, testable webhook endpoint.
---
## 1. Project layout
```
/src
/MyWebhookApi
Program.cs
/Controllers
WebhookController.cs
/Models
MyWebhookPayload.cs
/Services
IWebhookSignatureValidator.cs
HmacSha256SignatureValidator.cs
/Extensions
ServiceCollectionExtensions.cs
```
---
## 2. The payload model
```csharp
// /Models/MyWebhookPayload.cs
using System.Text.Json.Serialization;
namespace MyWebhookApi.Models;
/// <summary>
/// Example payload that the webhook provider sends.
/// Adjust the properties to match the actual JSON you expect.
/// </summary>
public sealed class MyWebhookPayload
{
[JsonPropertyName("event")]
public string? Event { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
[JsonPropertyName("data")]
public JsonElement Data { get; set; } // keep it generic, or replace with a concrete type
}
```
---
## 3. Signature‑validation service
```csharp
// /Services/IWebhookSignatureValidator.cs
namespace MyWebhookApi.Services;
/// <summary>
/// Abstracts the HMAC verification logic so it can be unit‑tested or swapped out.
/// </summary>
public interface IWebhookSignatureValidator
{
/// <summary>
/// Returns true if the supplied signature matches the HMAC of the request body.
/// </summary>
/// <param name="requestBody">Raw request body (UTF‑8 bytes).</param>
/// <param name="signatureHeader">Value of the X‑Signature header sent by the provider.</param>
/// <returns>True when the signature is valid.</returns>
bool IsValid(byte[] requestBody, string? signatureHeader);
}
```
```csharp
// /Services/HmacSha256SignatureValidator.cs
using System.Security.Cryptography;
using System.Text;
namespace MyWebhookApi.Services;
/// <summary>
/// Implements HMAC‑SHA256 verification. The provider must compute:
/// signature = Base64Encode( HMAC_SHA256(secret, requestBody) )
/// and send it in the X‑Signature header.
/// </summary>
public sealed class HmacSha256SignatureValidator : IWebhookSignatureValidator
{
private readonly byte[] _secretKey;
public HmacSha256SignatureValidator(string secretKey)
{
if (string.IsNullOrWhiteSpace(secretKey))
throw new ArgumentException("Secret key cannot be null or whitespace.", nameof(secretKey));
// Store the secret as UTF‑8 bytes – this is the most common convention.
_secretKey = Encoding.UTF8.GetBytes(secretKey);
}
public bool IsValid(byte[] requestBody, string? signatureHeader)
{
if (string.IsNullOrWhiteSpace(signatureHeader))
return false; // missing header
// Compute HMAC‑SHA256 of the raw request body.
using var hmac = new HMACSHA256(_secretKey);
var computedHash = hmac.ComputeHash(requestBody);
var computedSignature = Convert.ToBase64String(computedHash);
// Use a constant‑time comparison to avoid timing attacks.
return FixedTimeEquals(computedSignature, signatureHeader);
}
// Constant‑time string comparison (avoids early‑exit timing attacks)
private static bool FixedTimeEquals(string a, string b)
{
if (a.Length != b.Length)
return false;
var result = 0;
for (int i = 0; i < a.Length; i++)
result |= a[i] ^ b[i];
return result == 0;
}
}
```
---
## 4. Register the service in DI
```csharp
// /Extensions/ServiceCollectionExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using MyWebhookApi.Services;
namespace MyWebhookApi.Extensions;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the HMAC‑SHA256 signature validator to the container.
/// The secret key is read from configuration (appsettings.json, env vars, etc.).
/// </summary>
public static IServiceCollection AddWebhookSignatureValidation(this IServiceCollection services, IConfiguration configuration)
{
var secret = configuration["Webhook:HmacSecret"];
if (string.IsNullOrWhiteSpace(secret))
throw new InvalidOperationException("Webhook:HmacSecret must be configured.");
services.AddSingleton<IWebhookSignatureValidator>(new HmacSha256SignatureValidator(secret));
return services;
}
}
```
Add the configuration key to **appsettings.json** (or supply via environment variable):
```json
{
"Webhook": {
"HmacSecret": "YOUR_SUPER_SECRET_KEY_GOES_HERE"
},
// other settings...
}
```
---
## 5. The controller (the actual endpoint)
```csharp
// /Controllers/WebhookController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MyWebhookApi.Models;
using MyWebhookApi.Services;
namespace MyWebhookApi.Controllers;
/// <summary>
/// Receives webhook POSTs at /api/webhook.
/// </summary>
[ApiController]
[Route("api/[controller]")]
public sealed class WebhookController : ControllerBase
{
private readonly ILogger<WebhookController> _logger;
private readonly IWebhookSignatureValidator _signatureValidator;
public WebhookController(ILogger<WebhookController> logger,
IWebhookSignatureValidator signatureValidator)
{
_logger = logger;
_signatureValidator = signatureValidator;
}
/// <summary>
/// POST /api/webhook
/// </summary>
[HttpPost]
public async Task<IActionResult> Receive()
{
// 1️⃣ Read the raw body (as bytes) – needed for HMAC verification.
await using var memoryStream = new MemoryStream();
await Request.Body.CopyToAsync(memoryStream);
var requestBodyBytes = memoryStream.ToArray();
// 2️⃣ Grab the signature header (the provider must send it).
var signatureHeader = Request.Headers["X-Signature"].FirstOrDefault();
// 3️⃣ Verify the signature.
if (!_signatureValidator.IsValid(requestBodyBytes, signatureHeader))
{
_logger.LogWarning("Webhook signature validation failed. Header: {Header}", signatureHeader);
return Unauthorized(); // 401
}
// 4️⃣ At this point the request is authentic – deserialize the payload.
MyWebhookPayload? payload;
try
{
payload = System.Text.Json.JsonSerializer.Deserialize<MyWebhookPayload>(requestBodyBytes);
}
catch (System.Text.Json.JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize webhook payload.");
return BadRequest("Invalid JSON payload.");
}
if (payload is null)
{
_logger.LogError("Deserialized payload is null.");
return BadRequest("Empty payload.");
}
// 5️⃣ Process the payload (business logic goes here).
// -----------------------------------------------------------------
// Example: just log it. Replace with your own handling (e.g. enqueue,
// update DB, call other services, etc.).
_logger.LogInformation("Received webhook: Event={Event}, Timestamp={Timestamp}", payload.Event, payload.Timestamp);
// -----------------------------------------------------------------
// 6️⃣ Respond with 200 OK (or 202 Accepted if you process asynchronously).
return Ok();
}
}
```
**Why we read the body into a byte array first**
* The HMAC must be calculated over the **exact** bytes the sender signed.
* ASP.NET Core’s model binding reads the request stream once, so we capture the raw bytes, verify the signature, then deserialize from the same byte array.
---
## 6. Wire everything up in `Program.cs`
```csharp
// Program.cs
using MyWebhookApi.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add logging, configuration, etc.
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
// Register services
builder.Services.AddControllers();
builder.Services.AddWebhookSignatureValidation(builder.Configuration);
// (Optional) Add Swagger for testing
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Global exception handling – optional but recommended
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
var exception = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.Error;
logger.LogError(exception, "Unhandled exception");
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Internal Server Error");
});
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
```
---
## 7. Unit‑testing the validator (example with xUnit)
```csharp
// Tests/HmacSha256SignatureValidatorTests.cs
using MyWebhookApi.Services;
using Xunit;
public class HmacSha256SignatureValidatorTests
{
private const string Secret = "test-secret";
[Fact]
public void ValidSignature_ReturnsTrue()
{
// Arrange
var payload = System.Text.Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
var validator = new HmacSha256SignatureValidator(Secret);
// Compute the expected signature the same way the provider would.
using var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(Secret));
var hash = hmac.ComputeHash(payload);
var expectedSignature = Convert.ToBase64String(hash);
// Act
var result = validator.IsValid(payload, expectedSignature);
// Assert
Assert.True(result);
}
[Fact]
public void InvalidSignature_ReturnsFalse()
{
var payload = System.Text.Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
var validator = new HmacSha256SignatureValidator(Secret);
var result = validator.IsValid(payload, "totally-wrong-signature");
Assert.False(result);
}
[Fact]
public void MissingHeader_ReturnsFalse()
{
var payload = System.Text.Encoding.UTF8.GetBytes("{}");
var validator = new HmacSha256SignatureValidator(Secret);
var result = validator.IsValid(payload, null);
Assert.False(result);
}
}
```
Run `dotnet test` – all should pass.
---
## 8. How the provider should sign the request
```text
// Pseudo‑code (any language)
secret = "YOUR_SUPER_SECRET_KEY_GOES_HERE"
body = JSON.stringify(payload) // raw UTF‑8 bytes
hash = HMAC_SHA256(secret, body)
signature = Base64Encode(hash)
// HTTP request
POST /api/webhook HTTP/1.1
Content-Type: application/json
X-Signature: {signature}
Content-Length: ...
{body}
```
If you control the provider, make sure it uses **exactly** the same algorithm (HMAC‑SHA256, UTF‑8 encoding, Base64 output) and includes the header name `X-Signature`. Adjust the header name in the controller if you need a different one.
---
## 9. Production tips
| Concern | Recommendation |
|---------|----------------|
| **Secret storage** | Never hard‑code the secret. Store it in a secret manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) or at least in an environment variable (`Webhook__HmacSecret`). |
| **Replay attacks** | Include a timestamp in the payload (e.g., `timestamp` field) and reject messages older than a few minutes. |
| **Idempotency** | Webhooks can be retried. Store a hash of the payload or a unique event ID and ignore duplicates. |
| **Logging** | Log **only** the fact that verification succeeded/failed, not the raw signature or secret. |
| **Performance** | The validator is cheap; however, if you expect thousands of requests per second, consider pooling the `HMACSHA256` instance via `ArrayPool<byte>` or a custom `IObjectPool`. |
| **Testing** | Use the `IWebhookSignatureValidator` abstraction to mock verification in integration tests for the controller. |
---
## 10. Quick start checklist
1. **Create** a new ASP.NET Core Web API project (`dotnet new webapi -n MyWebhookApi`).
2. **Add** the files above in the appropriate folders.
3. **Set** `Webhook:HmacSecret` in `appsettings.Development.json` or via environment variable.
4. **Run** the API (`dotnet run`).
5. **Send** a test request (e.g., with `curl` or Postman) using the same secret to verify that a `200 OK` is returned only when the signature matches.
```bash
payload='{"event":"order.created","timestamp":1696526400,"data":{"orderId":123}}'
secret='YOUR_SUPER_SECRET_KEY_GOES_HERE'
signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -binary | base64)
curl -X POST https://localhost:7185/api/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: $signature" \
-d "$payload" -k
```
You should see `200 OK` and a log entry like:
```
[Information] Received webhook: Event=order.created, Timestamp=1696526400
```
If you tamper with the payload or omit the header, the endpoint returns `401 Unauthorized`.
---
**That’s it!** You now have a fully functional webhook delivery endpoint with robust HMAC signature verification, ready to be dropped into any ASP.NET Core service. Feel free to adapt the model, header name, or hashing algorithm to match the exact contract of the webhook provider you are integrating with. Happy coding!
Enhanced response (with ZPI)
**Solution Overview**
* A **POST** endpoint `/api/webhooks/{topic}` receives the raw request body.
* The caller must send an `X‑Signature` header that contains a Base64‑encoded HMAC‑SHA256 of the request body using a shared secret.
* The endpoint reads the body **as a stream**, computes the HMAC, compares it with the header value in a time‑constant way and, if it matches, forwards the payload to a **scoped** `IWebhookHandler` implementation that contains the business logic for the specific `topic`.
* All services are registered with the built‑in DI container; the handler is registered **scoped** so that per‑request state (e.g. a DB context) can be injected.
* `ILogger<T>` is injected into the controller and the handler for structured logging.
The code below is a complete, production‑ready example that can be dropped into a new or existing ASP.NET Core 8 (or later) project.
---
## 1. Domain model
```csharp
// Represents the payload that the handler will receive.
// In a real project you would create a concrete type per topic.
public record WebhookPayload(string Topic, string Body);
```
---
## 2. HMAC verification service
```csharp
using System.Security.Cryptography;
using System.Text;
public interface IWebhookSignatureValidator
{
/// <summary>
/// Validates that the supplied signature matches the HMAC‑SHA256 of the body.
/// </summary>
/// <param name="body">Raw request body (UTF‑8).</param>
/// <param name="signature">Base64‑encoded signature from the X‑Signature header.</param>
/// <returns>True if the signature is valid.</returns>
bool IsValid(string body, string signature);
}
public sealed class WebhookSignatureValidator : IWebhookSignatureValidator
{
private readonly byte[] _secretKey;
public WebhookSignatureValidator(string secretKey)
{
if (string.IsNullOrWhiteSpace(secretKey))
throw new ArgumentException("Secret key must be provided.", nameof(secretKey));
_secretKey = Encoding.UTF8.GetBytes(secretKey);
}
public bool IsValid(string body, string signature)
{
if (string.IsNullOrEmpty(signature))
return false;
// Compute HMAC‑SHA256 of the body
using var hmac = new HMACSHA256(_secretKey);
var bodyBytes = Encoding.UTF8.GetBytes(body);
var computedHash = hmac.ComputeHash(bodyBytes);
var computedBase64 = Convert.ToBase64String(computedHash);
// Constant‑time comparison to avoid timing attacks
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedBase64),
Encoding.UTF8.GetBytes(signature));
}
}
```
*The validator is registered as a **singleton** because it only holds the secret key.*
---
## 3. Scoped webhook handler contract
```csharp
public interface IWebhookHandler
{
/// <summary>
/// Handles a verified webhook payload.
/// </summary>
Task HandleAsync(WebhookPayload payload, CancellationToken cancellationToken);
}
```
### Example implementation (you would have one per topic)
```csharp
using Microsoft.Extensions.Logging;
public sealed class SampleWebhookHandler : IWebhookHandler
{
private readonly ILogger<SampleWebhookHandler> _logger;
// Example of a scoped dependency (e.g. EF Core DbContext)
// private readonly MyDbContext _db;
public SampleWebhookHandler(ILogger<SampleWebhookHandler> logger/*, MyDbContext db*/)
{
_logger = logger;
// _db = db;
}
public async Task HandleAsync(WebhookPayload payload, CancellationToken cancellationToken)
{
_logger.LogInformation("Processing webhook for topic {Topic}", payload.Topic);
// Business logic goes here.
// await _db.SaveAsync(...);
await Task.CompletedTask;
}
}
```
---
## 4. Controller (routing middleware)
```csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("api/webhooks")]
public sealed class WebhookController : ControllerBase
{
private const string SignatureHeaderName = "X-Signature";
private readonly IWebhookSignatureValidator _validator;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<WebhookController> _logger;
public WebhookController(
IWebhookSignatureValidator validator,
IServiceProvider serviceProvider,
ILogger<WebhookController> logger)
{
_validator = validator;
_serviceProvider = serviceProvider;
_logger = logger;
}
[HttpPost("{topic}")]
public async Task<IActionResult> ReceiveAsync(
[FromRoute] string topic,
CancellationToken cancellationToken)
{
// 1️⃣ Read raw body (do not let MVC model binding consume it)
string body;
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true))
{
body = await reader.ReadToEndAsync(cancellationToken);
}
// 2️⃣ Extract signature header
if (!Request.Headers.TryGetValue(SignatureHeaderName, out var signatureValues))
{
_logger.LogWarning("Missing {Header} header for topic {Topic}", SignatureHeaderName, topic);
return BadRequest("Missing signature header.");
}
var signature = signatureValues.FirstOrDefault();
if (string.IsNullOrEmpty(signature))
{
_logger.LogWarning("Empty {Header} header for topic {Topic}", SignatureHeaderName, topic);
return BadRequest("Empty signature header.");
}
// 3️⃣ Verify HMAC
if (!_validator.IsValid(body, signature))
{
_logger.LogWarning("Invalid HMAC signature for topic {Topic}", topic);
return Unauthorized("Invalid signature.");
}
// 4️⃣ Resolve the appropriate handler (scoped)
var handler = ResolveHandler(topic);
if (handler is null)
{
_logger.LogWarning("No handler registered for topic {Topic}", topic);
return NotFound($"No handler for topic '{topic}'.");
}
// 5️⃣ Dispatch to handler
var payload = new WebhookPayload(topic, body);
try
{
await handler.HandleAsync(payload, cancellationToken);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling webhook for topic {Topic}", topic);
return StatusCode(StatusCodes.Status500InternalServerError, "Handler error.");
}
}
private IWebhookHandler? ResolveHandler(string topic)
{
// Simple convention: "sample" => SampleWebhookHandler.
// In a real app you could use a dictionary, attribute, or MediatR.
return topic.ToLowerInvariant() switch
{
"sample" => _serviceProvider.GetRequiredService<SampleWebhookHandler>(),
_ => null
};
}
}
```
*Key points*
* The controller reads the **raw request body** so the HMAC is calculated on the exact bytes the sender signed.
* The signature is expected in the `X‑Signature` header and must be Base64‑encoded.
* `CryptographicOperations.FixedTimeEquals` provides a constant‑time comparison.
* The handler is resolved from the **scoped** service provider, guaranteeing a new instance per request (and allowing other scoped services like a DB context).
---
## 5. DI registration (Program.cs)
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// 1️⃣ Configuration – secret key can be stored in user‑secrets, env vars, etc.
var secretKey = builder.Configuration["Webhook:HmacSecret"] ??
throw new InvalidOperationException("Webhook secret not configured.");
// 2️⃣ Register services
builder.Services.AddSingleton<IWebhookSignatureValidator>(sp =>
new WebhookSignatureValidator(secretKey));
// Scoped handler(s)
builder.Services.AddScoped<SampleWebhookHandler>(); // implements IWebhookHandler
// If you have many handlers you could register them all as IWebhookHandler
// builder.Services.AddScoped<IWebhookHandler, SampleWebhookHandler>();
// MVC / minimal API
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers(); // routing middleware
app.Run();
```
*The secret key is read from configuration (`appsettings.json`, environment variables, Azure Key Vault, etc.).*
---
## 6. Example `appsettings.json`
```json
{
"Webhook": {
"HmacSecret": "my-very-secret-key"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
```
---
## 7. How to test
```bash
# Compute signature (C# example, but you can use any language)
payload='{"event":"order.created","id":123}'
secret='my-very-secret-key'
signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -binary | base64)
curl -X POST "http://localhost:5000/api/webhooks/sample" \
-H "Content-Type: application/json" \
-H "X-Signature: $signature" \
-d "$payload"
```
If the signature matches, the endpoint returns **200 OK** and the handler logs the processing. An invalid signature returns **401 Unauthorized**.
---
## 8. Summary of requirements satisfied
| Requirement | Implementation |
|-------------|----------------|
| **Routing middleware** | `app.MapControllers()` and `[HttpPost("{topic}")]` attribute. |
| **ILogger\<T> injection** | `ILogger<WebhookController>` and `ILogger<SampleWebhookHandler>` are injected via constructor. |
| **Scoped registration** | `SampleWebhookHandler` (and any other handlers) are registered with `AddScoped`. The controller resolves the handler from the request‑scoped `IServiceProvider`. |
| **HMAC verification** | `WebhookSignatureValidator` computes HMAC‑SHA256 with a secret key and uses constant‑time comparison. |
| **Error handling & logging** | Detailed logs for missing/invalid signatures, missing handlers, and handler exceptions; appropriate HTTP status codes returned. |
Drop the files into your project, adjust the secret key source and add concrete handlers for each webhook topic you need, and you have a secure, testable webhook delivery endpoint ready for production.
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-sonnet-4-6
SameV4qwen2.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.