From 06d8d13e190baea322f4fd440d08c1dc66e06116 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Tue, 30 Jun 2026 15:11:14 +0200 Subject: [PATCH] feat(event-subscriber): enforce the callback bearer before reading the body (refs #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check the Authorization header before deserializing the notification, and read/parse the body manually. NRC probes a new abonnement's callback with a request that has neither the configured auth nor a valid notification body, and refuses to register unless it gets a 401 (not a 400) — ADR-0007. Mirrors the verify harness's sink contract. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../EventSubscriber.Api/Program.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/services/event-subscriber/EventSubscriber.Api/Program.cs b/services/event-subscriber/EventSubscriber.Api/Program.cs index a2f7e55..1b8b981 100644 --- a/services/event-subscriber/EventSubscriber.Api/Program.cs +++ b/services/event-subscriber/EventSubscriber.Api/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using EventSubscriber.Application; using Projection.ReadModel; @@ -22,18 +23,23 @@ await app.Services.MigrateProjectionAsync(); app.MapGet("/health", () => "Healthy"); // The NRC abonnement callback. Open Notificaties POSTs a notification here; we project it. -// Auth-on-callback is mandatory: without the configured Authorization, return 401 (NRC's -// registration probe depends on this — ADR-0007). +// Auth-on-callback is mandatory: the auth check runs *before* the body is read, so NRC's +// registration probe (a POST without the configured Authorization, and without a valid +// notification body) gets the 401 it requires rather than a 400 (ADR-0007). app.MapPost("/notifications", async ( HttpRequest request, - NotificationDto body, NotificationProjector projector, CancellationToken ct) => { if (request.Headers.Authorization != webhookToken) return Results.Unauthorized(); - await projector.HandleAsync(body.ToNotification(), ct); + var dto = await JsonSerializer.DeserializeAsync( + request.Body, SerializerOptions, ct); + if (dto is null) + return Results.BadRequest(); + + await projector.HandleAsync(dto.ToNotification(), ct); return Results.NoContent(); }); @@ -53,4 +59,8 @@ public sealed record NotificationDto(string Kanaal, string Resource, string Acti public Notification ToNotification() => new(Kanaal, Resource, Actie, ResourceUrl); } -public partial class Program; +public partial class Program +{ + // NRC sends camelCase JSON; match it case-insensitively. + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); +}