From 1ce5d9150d5b1afd8730079dc7aea5f31e90aacb Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Tue, 5 May 2026 12:27:22 +0200 Subject: [PATCH] feat(auth): require DID owner JWT to manage webhooks Both POST /:did/webhooks and DELETE /:did/webhooks were unauthenticated: anyone could register a webhook for someone else's DID (privacy leak) or wipe a DID's webhook list (DoS on legitimate subscribers). Now both endpoints require a Bluesky session bearer token, verified end-to-end against the DID's PDS via the existing authenticateRequest helper, and the verified DID must match the URL :did. --- server.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/server.ts b/server.ts index 7bcae54..bab0941 100644 --- a/server.ts +++ b/server.ts @@ -7,6 +7,7 @@ import { getNotesByDids, type WebhookVerb, } from "./src/data/db.ts"; +import { authenticateRequest } from "./src/auth/verify.ts"; import { log } from "./src/log.ts"; const router = new Router(); @@ -89,6 +90,21 @@ const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const; router.post("/:did/webhooks", async (ctx) => { const { did } = ctx.params; + let verifiedDid: string; + try { + verifiedDid = await authenticateRequest( + ctx.request.headers.get("Authorization"), + ); + } catch { + ctx.response.status = 401; + ctx.response.body = { error: "Unauthorized" }; + return; + } + if (verifiedDid !== did) { + ctx.response.status = 403; + ctx.response.body = { error: "You can only manage your own webhooks" }; + return; + } const body = await ctx.request.body.json(); const { method, url, token, verb } = body ?? {}; if (!method || !url) { @@ -111,8 +127,23 @@ router.post("/:did/webhooks", async (ctx) => { ctx.response.body = subscriptions; }); -router.delete("/:did/webhooks", (ctx) => { +router.delete("/:did/webhooks", async (ctx) => { const { did } = ctx.params; + let verifiedDid: string; + try { + verifiedDid = await authenticateRequest( + ctx.request.headers.get("Authorization"), + ); + } catch { + ctx.response.status = 401; + ctx.response.body = { error: "Unauthorized" }; + return; + } + if (verifiedDid !== did) { + ctx.response.status = 403; + ctx.response.body = { error: "You can only manage your own webhooks" }; + return; + } deleteWebhooksByDid(did); ctx.response.status = 204; });