fix(line): return 200 for webhook verification requests without signature
LINE Platform sends POST {"events":[]} without an X-Line-Signature
header when the user clicks 'Verify' in the LINE Developers Console.
Both webhook.ts and monitor.ts rejected this with 400 'Missing
X-Line-Signature header', causing verification to fail.
Now detect the verification pattern (no signature + empty events array)
and return 200 OK immediately, while still requiring valid signatures
for all real webhook deliveries with non-empty events.
Fixes #16425
This commit is contained in:
committed by
Peter Steinberger
parent
3189430ad0
commit
4f2f641950
@@ -322,8 +322,24 @@ export async function monitorLineProvider(
|
||||
const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_MAX_BODY_BYTES);
|
||||
const signature = req.headers["x-line-signature"];
|
||||
|
||||
// Validate signature
|
||||
// LINE webhook verification sends POST {"events":[]} without a
|
||||
// signature header. Return 200 so the LINE Developers Console
|
||||
// "Verify" button succeeds.
|
||||
if (!signature || typeof signature !== "string") {
|
||||
try {
|
||||
const verifyBody = JSON.parse(rawBody) as WebhookRequestBody;
|
||||
if (Array.isArray(verifyBody.events) && verifyBody.events.length === 0) {
|
||||
logVerbose(
|
||||
"line: webhook verification request (empty events, no signature) — 200 OK",
|
||||
);
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON — fall through to the error below.
|
||||
}
|
||||
logVerbose("line: webhook missing X-Line-Signature header");
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
@@ -98,6 +98,48 @@ describe("createLineWebhookMiddleware", () => {
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 200 for verification request (empty events, no signature)", async () => {
|
||||
const onEvents = vi.fn(async () => {});
|
||||
const secret = "secret";
|
||||
const rawBody = JSON.stringify({ events: [] });
|
||||
const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents });
|
||||
|
||||
const req = {
|
||||
headers: {},
|
||||
body: rawBody,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects missing signature when events are non-empty", async () => {
|
||||
const onEvents = vi.fn(async () => {});
|
||||
const secret = "secret";
|
||||
const rawBody = JSON.stringify({ events: [{ type: "message" }] });
|
||||
const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents });
|
||||
|
||||
const req = {
|
||||
headers: {},
|
||||
body: rawBody,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Missing X-Line-Signature header" });
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects webhooks with signatures computed using wrong secret", async () => {
|
||||
const onEvents = vi.fn(async () => {});
|
||||
const correctSecret = "correct-secret";
|
||||
|
||||
@@ -39,13 +39,24 @@ export function createLineWebhookMiddleware(
|
||||
return async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
const signature = req.headers["x-line-signature"];
|
||||
const rawBody = readRawBody(req);
|
||||
|
||||
// LINE webhook verification sends POST {"events":[]} without a
|
||||
// signature header. Return 200 immediately so the LINE Developers
|
||||
// Console "Verify" button succeeds.
|
||||
if (!signature || typeof signature !== "string") {
|
||||
if (rawBody) {
|
||||
const body = parseWebhookBody(req, rawBody);
|
||||
if (body && Array.isArray(body.events) && body.events.length === 0) {
|
||||
logVerbose("line: webhook verification request (empty events, no signature) — 200 OK");
|
||||
res.status(200).json({ status: "ok" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.status(400).json({ error: "Missing X-Line-Signature header" });
|
||||
return;
|
||||
}
|
||||
|
||||
const rawBody = readRawBody(req);
|
||||
if (!rawBody) {
|
||||
res.status(400).json({ error: "Missing raw request body for signature verification" });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user