WebhookGuard.AspNetCore
0.1.2
dotnet add package WebhookGuard.AspNetCore --version 0.1.2
NuGet\Install-Package WebhookGuard.AspNetCore -Version 0.1.2
<PackageReference Include="WebhookGuard.AspNetCore" Version="0.1.2" />
<PackageVersion Include="WebhookGuard.AspNetCore" Version="0.1.2" />
<PackageReference Include="WebhookGuard.AspNetCore" />
paket add WebhookGuard.AspNetCore --version 0.1.2
#r "nuget: WebhookGuard.AspNetCore, 0.1.2"
#:package WebhookGuard.AspNetCore@0.1.2
#addin nuget:?package=WebhookGuard.AspNetCore&version=0.1.2
#tool nuget:?package=WebhookGuard.AspNetCore&version=0.1.2
webhook-guard
For SaaS backends receiving Stripe, GitHub, or Svix webhooks: verify signatures, reject stale and replayed deliveries, route typed events. One receiver pipeline in ASP.NET Core.
using System.Text;
using WebhookGuard;
var replay = new InMemoryReplayStore();
var secret = builder.Configuration["Stripe:Secret"]!;
var tolerance = TimeSpan.FromMinutes(5);
app.MapGuardedWebhook<StripeEvent>("payment.succeeded",
async (raw, ctx) =>
{
var payload = Encoding.UTF8.GetString(raw);
var header = ctx.Request.Headers["Stripe-Signature"].ToString();
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (!StripeVerifier.Verify(payload, header, secret, now, tolerance))
return false;
var evt = System.Text.Json.JsonSerializer.Deserialize<StripeEvent>(raw)!;
var ok = await replay.TryAddAsync(
ReplayKeys.For(evt.Id, header), DateTimeOffset.UtcNow + tolerance);
return ok;
},
async (evt, _) => await orders.MarkPaidAsync(evt.Id));
public sealed record StripeEvent(string Id, string Type);
Install: dotnet add package WebhookGuard.AspNetCore. Forged body: 401. Stale
timestamp: 401. Same delivery twice: 401. Only then does your handler run.
Replay keys need a stable message id plus the signature. Stripe: the event
id from the payload. GitHub: the X-GitHub-Delivery header. Svix: the
svix-id header. ReplayKeys.For(id, signature) combines them.
The three checks, and why all three
Most receivers verify the signature and stop. Two holes stay open:
- Skew. A valid signed payload stays valid forever. Without a timestamp
window, anyone holding an old delivery can re-fire it. Every verifier here
takes the current time plus a tolerance (5 minutes default) and rejects
anything older. Covered per scheme in
VectorsTestsandSkewTests. - Replay inside the window. A delivery sent twice within 5 minutes passes
signature plus skew both times.
IReplayStore.TryAddAsync(in-memory default,PostgresReplayStoreoptional) drops the second copy. Covered byReplayTests. - Unverified routes. The plain
MapWebhookmapper is marked[Obsolete]because it routes without checking anything.MapGuardedWebhookreads the raw body, runs your async verifier, and only then deserializes.
All comparisons run in constant time. Stripe secrets stay raw UTF-8 (even
whsec_ test-mode ones, exactly as Stripe sends them). Svix whsec_ secrets
get base64-decoded per their spec, and anything that fails to decode fails
closed. Stripe accepts any valid v1 signature, so key rotation never breaks
verification. Covered by Stripe_RotationSecondV1_Accepts.
Postgres replay store
await PostgresReplayStore.EnsureTableAsync(connString);
// per request:
var pg = new PostgresReplayStore(connString);
Call PostgresReplayStore.PurgeExpiredAsync(connString) on a timer. The
table doesn't clean itself.
Threat model, plainly
Stops: forged payloads, stale replays, duplicate deliveries, wrong-type dispatch. Doesn't stop: a leaked signing secret (rotate it), a compromised sender, or your handler's own bugs. Hash comparisons don't make a bad secret good.
Not Svix, not verihook
Svix is hosted delivery infrastructure. This is the receiver side Svix users
still hand-roll. verihook and webhook-verify check signatures; guard adds
skew windows, replay stores, and the guarded route tying it together. If you
only need "is this signature valid", those smaller packages fit. If you need
the full receiver, this one does.
Tests
20 xUnit tests: known vectors per scheme, skew rejects, tamper rejects, replay rejects, rotation acceptance, live HTTP route tests.
Part of a trilogy: webhook-guard (verify at ingress) → idempotency-keys (dedupe) → pg-outbox (publish at egress).
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Npgsql (>= 9.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.2 | 89 | 9/12/2026 |