diff --git a/lib/middleware.ts b/lib/middleware.ts index fa9ba349b..1e6fefdbd 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -62,7 +62,13 @@ export default function middleware(config: Types.MiddlewareConfig): Middleware { } })(); - if (!validateSignature(body, secret, signature)) { + const shouldSkipVerification = + config.skipSignatureVerification && config.skipSignatureVerification(); + + if ( + !shouldSkipVerification && + !validateSignature(body, secret, signature) + ) { next( new SignatureValidationFailed("signature validation failed", { signature, diff --git a/lib/types.ts b/lib/types.ts index 532987958..dc77d4512 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -15,6 +15,14 @@ export interface ClientConfig extends Config { export interface MiddlewareConfig extends Config { channelSecret: string; + + // skipSignatureValidation is a function that determines whether to skip + // webhook signature verification. + // + // If the function returns true, the signature verification step is skipped. + // This can be useful in scenarios such as when you're in the process of updating + // the channel secret and need to temporarily bypass verification to avoid disruptions. + skipSignatureVerification?: () => boolean; } export type Profile = { diff --git a/test/helpers/test-server.ts b/test/helpers/test-server.ts index 2487ff38d..d4f67e8b5 100644 --- a/test/helpers/test-server.ts +++ b/test/helpers/test-server.ts @@ -10,7 +10,8 @@ import { } from "../../lib/exceptions.js"; import * as finalhandler from "finalhandler"; -let server: Server | null = null; +// Use a map to store multiple server instances +let servers: Map = new Map(); function listen(port: number, middleware?: express.RequestHandler) { const app = express(); @@ -77,17 +78,40 @@ function listen(port: number, middleware?: express.RequestHandler) { ); return new Promise(resolve => { - server = app.listen(port, () => resolve(undefined)); + const server = app.listen(port, () => resolve(undefined)); + servers.set(port, server); }); } -function close() { +function close(port?: number) { return new Promise(resolve => { - if (!server) { - return resolve(undefined); - } + if (port !== undefined) { + const server = servers.get(port); + if (!server) { + return resolve(undefined); + } + + server.close(() => { + servers.delete(port); + resolve(undefined); + }); + } else { + // Close all servers if no port is specified + if (servers.size === 0) { + return resolve(undefined); + } + + const promises = Array.from(servers.entries()).map(([port, server]) => { + return new Promise(resolveServer => { + server.close(() => { + servers.delete(port); + resolveServer(undefined); + }); + }); + }); - server.close(() => resolve(undefined)); + Promise.all(promises).then(() => resolve(undefined)); + } }); } diff --git a/test/middleware.spec.ts b/test/middleware.spec.ts index ec8911dad..fcc21b06f 100644 --- a/test/middleware.spec.ts +++ b/test/middleware.spec.ts @@ -53,8 +53,72 @@ describe("middleware test", () => { beforeAll(() => { listen(TEST_PORT, m); }); + + describe("With skipSignatureVerification functionality", () => { + let serverPort: number; + + const createClient = (port: number) => + new HTTPClient({ + baseURL: `http://localhost:${port}`, + defaultHeaders: { + "X-Line-Signature": "invalid_signature", + }, + }); + + afterEach(() => { + if (serverPort) { + close(serverPort); + } + }); + + it("should skip signature verification when skipSignatureVerification returns true", async () => { + serverPort = TEST_PORT + 1; + const m = middleware({ + channelSecret: "test_channel_secret", + skipSignatureVerification: () => true, + }); + await listen(serverPort, m); + + const client = createClient(serverPort); + + await client.post("/webhook", { + events: [webhook], + destination: DESTINATION, + }); + + const req = getRecentReq(); + deepEqual(req.body.destination, DESTINATION); + deepEqual(req.body.events, [webhook]); + }); + + it("should skip signature verification when skipSignatureVerification returns false", async () => { + serverPort = TEST_PORT + 2; + const m = middleware({ + channelSecret: "test_channel_secret", + skipSignatureVerification: () => false, + }); + await listen(serverPort, m); + + const client = createClient(serverPort); + + try { + await client.post("/webhook", { + events: [webhook], + destination: DESTINATION, + }); + ok(false, "Expected to throw an error due to invalid signature"); + } catch (err) { + if (err instanceof HTTPError) { + equal(err.statusCode, 401); + } else { + throw err; + } + } + }); + }); + afterAll(() => { - close(); + close(TEST_PORT); }); describe("Succeeds on parsing valid request", () => {