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.
This commit is contained in:
33
server.ts
33
server.ts
@@ -7,6 +7,7 @@ import {
|
|||||||
getNotesByDids,
|
getNotesByDids,
|
||||||
type WebhookVerb,
|
type WebhookVerb,
|
||||||
} from "./src/data/db.ts";
|
} from "./src/data/db.ts";
|
||||||
|
import { authenticateRequest } from "./src/auth/verify.ts";
|
||||||
import { log } from "./src/log.ts";
|
import { log } from "./src/log.ts";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
@@ -89,6 +90,21 @@ const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const;
|
|||||||
|
|
||||||
router.post("/:did/webhooks", async (ctx) => {
|
router.post("/:did/webhooks", async (ctx) => {
|
||||||
const { did } = ctx.params;
|
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 body = await ctx.request.body.json();
|
||||||
const { method, url, token, verb } = body ?? {};
|
const { method, url, token, verb } = body ?? {};
|
||||||
if (!method || !url) {
|
if (!method || !url) {
|
||||||
@@ -111,8 +127,23 @@ router.post("/:did/webhooks", async (ctx) => {
|
|||||||
ctx.response.body = subscriptions;
|
ctx.response.body = subscriptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/:did/webhooks", (ctx) => {
|
router.delete("/:did/webhooks", async (ctx) => {
|
||||||
const { did } = ctx.params;
|
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);
|
deleteWebhooksByDid(did);
|
||||||
ctx.response.status = 204;
|
ctx.response.status = 204;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user