Skip to content

Commit 396aaf6

Browse files
committed
fix: webhook token
1 parent ec41be5 commit 396aaf6

1 file changed

Lines changed: 46 additions & 11 deletions

File tree

src/routes/webhooks/jira.post.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineEventHandler, readBody, getHeader, createError } from "h3";
1+
import { createHmac, timingSafeEqual } from "node:crypto";
2+
import { defineEventHandler, readRawBody, getHeader, createError } from "h3";
23
import { env } from "../../../env.js";
34
import { createAdapters } from "../../lib/adapters.js";
45
import { dispatchTicket } from "../../lib/dispatch.js";
@@ -8,17 +9,25 @@ import { logger } from "../../lib/logger.js";
89
* Jira webhook handler — triggers the same dispatch logic as the cron poller.
910
*
1011
* Configure in Jira (Settings → System → Webhooks) with:
11-
* URL: https://<your-domain>/webhooks/jira
12-
* Headers: Authorization: Bearer <JIRA_WEBHOOK_SECRET>
13-
* Events: Issue updated
12+
* URL: https://<your-domain>/webhooks/jira
13+
* Secret: <JIRA_WEBHOOK_SECRET>
14+
* Events: Issue updated
15+
*
16+
* Jira signs the payload with HMAC-SHA256 and sends it in the
17+
* X-Hub-Signature header (format: "sha256=<hex>").
1418
*
1519
* The webhook fires immediately when a ticket is moved to the AI column,
1620
* eliminating the up-to-1-minute polling delay.
1721
*/
1822
export default defineEventHandler(async (event) => {
19-
verifyWebhookAuth(event);
23+
const rawBody = await readRawBody(event, "utf8");
24+
25+
verifyWebhookSignature(
26+
rawBody ?? "",
27+
getHeader(event, "x-hub-signature"),
28+
);
2029

21-
const body = await readBody(event);
30+
const body = rawBody ? JSON.parse(rawBody) : {};
2231

2332
const ticketKey = extractTicketKey(body);
2433
if (!ticketKey) {
@@ -62,16 +71,42 @@ export default defineEventHandler(async (event) => {
6271
});
6372

6473
// ---------------------------------------------------------------------------
65-
// Auth
74+
// Auth — HMAC-SHA256 signature verification
6675
// ---------------------------------------------------------------------------
6776

68-
function verifyWebhookAuth(event: Parameters<typeof getHeader>[0]): void {
77+
/**
78+
* Verify the X-Hub-Signature header sent by Jira Cloud.
79+
*
80+
* Jira computes HMAC-SHA256 of the raw request body using the webhook
81+
* secret and sends it as "sha256=<hex>" in the X-Hub-Signature header.
82+
*
83+
* When JIRA_WEBHOOK_SECRET is not set, verification is skipped (open access).
84+
*/
85+
function verifyWebhookSignature(
86+
rawBody: string,
87+
signatureHeader: string | undefined,
88+
): void {
6989
if (!env.JIRA_WEBHOOK_SECRET) return;
7090

71-
const headerSecret = getHeader(event, "authorization")?.replace(/^Bearer\s+/i, "");
72-
if (headerSecret === env.JIRA_WEBHOOK_SECRET) return;
91+
if (!signatureHeader) {
92+
throw createError({ statusCode: 401, statusMessage: "Missing X-Hub-Signature header" });
93+
}
94+
95+
const [method, receivedSig] = signatureHeader.split("=", 2);
96+
if (!method || !receivedSig) {
97+
throw createError({ statusCode: 401, statusMessage: "Malformed X-Hub-Signature header" });
98+
}
7399

74-
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
100+
const expectedSig = createHmac(method, env.JIRA_WEBHOOK_SECRET)
101+
.update(rawBody, "utf8")
102+
.digest("hex");
103+
104+
const a = Buffer.from(receivedSig, "hex");
105+
const b = Buffer.from(expectedSig, "hex");
106+
107+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
108+
throw createError({ statusCode: 401, statusMessage: "Invalid webhook signature" });
109+
}
75110
}
76111

77112
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)