Skip to content

Commit 4234b5a

Browse files
authored
Allow to skip signature verification (#1398)
## Changes - Allow skipping signature verification for webhooks ## Motivation The signature returned with webhooks is calculated using a single channel secret. If the bot owner changes their channel secret, the signature for webhooks starts being calculated using the new channel secret. To avoid signature verification failures, the bot owner must update the channel secret on their server, which is used for signature verification. However, if there is a timing mismatch in the update—and such a mismatch is almost unavoidable—verification will fail during that period. In such cases, having an option to skip signature verification for webhooks would be a convenient way to avoid these issues.
1 parent c9b2e1f commit 4234b5a

File tree

4 files changed

+111
-9
lines changed

4 files changed

+111
-9
lines changed

lib/middleware.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ export default function middleware(config: Types.MiddlewareConfig): Middleware {
6262
}
6363
})();
6464

65-
if (!validateSignature(body, secret, signature)) {
65+
const shouldSkipVerification =
66+
config.skipSignatureVerification && config.skipSignatureVerification();
67+
68+
if (
69+
!shouldSkipVerification &&
70+
!validateSignature(body, secret, signature)
71+
) {
6672
next(
6773
new SignatureValidationFailed("signature validation failed", {
6874
signature,

lib/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export interface ClientConfig extends Config {
1515

1616
export interface MiddlewareConfig extends Config {
1717
channelSecret: string;
18+
19+
// skipSignatureValidation is a function that determines whether to skip
20+
// webhook signature verification.
21+
//
22+
// If the function returns true, the signature verification step is skipped.
23+
// This can be useful in scenarios such as when you're in the process of updating
24+
// the channel secret and need to temporarily bypass verification to avoid disruptions.
25+
skipSignatureVerification?: () => boolean;
1826
}
1927

2028
export type Profile = {

test/helpers/test-server.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
} from "../../lib/exceptions.js";
1111
import * as finalhandler from "finalhandler";
1212

13-
let server: Server | null = null;
13+
// Use a map to store multiple server instances
14+
let servers: Map<number, Server> = new Map();
1415

1516
function listen(port: number, middleware?: express.RequestHandler) {
1617
const app = express();
@@ -77,17 +78,40 @@ function listen(port: number, middleware?: express.RequestHandler) {
7778
);
7879

7980
return new Promise(resolve => {
80-
server = app.listen(port, () => resolve(undefined));
81+
const server = app.listen(port, () => resolve(undefined));
82+
servers.set(port, server);
8183
});
8284
}
8385

84-
function close() {
86+
function close(port?: number) {
8587
return new Promise(resolve => {
86-
if (!server) {
87-
return resolve(undefined);
88-
}
88+
if (port !== undefined) {
89+
const server = servers.get(port);
90+
if (!server) {
91+
return resolve(undefined);
92+
}
93+
94+
server.close(() => {
95+
servers.delete(port);
96+
resolve(undefined);
97+
});
98+
} else {
99+
// Close all servers if no port is specified
100+
if (servers.size === 0) {
101+
return resolve(undefined);
102+
}
103+
104+
const promises = Array.from(servers.entries()).map(([port, server]) => {
105+
return new Promise(resolveServer => {
106+
server.close(() => {
107+
servers.delete(port);
108+
resolveServer(undefined);
109+
});
110+
});
111+
});
89112

90-
server.close(() => resolve(undefined));
113+
Promise.all(promises).then(() => resolve(undefined));
114+
}
91115
});
92116
}
93117

test/middleware.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,72 @@ describe("middleware test", () => {
5353
beforeAll(() => {
5454
listen(TEST_PORT, m);
5555
});
56+
57+
describe("With skipSignatureVerification functionality", () => {
58+
let serverPort: number;
59+
60+
const createClient = (port: number) =>
61+
new HTTPClient({
62+
baseURL: `http://localhost:${port}`,
63+
defaultHeaders: {
64+
"X-Line-Signature": "invalid_signature",
65+
},
66+
});
67+
68+
afterEach(() => {
69+
if (serverPort) {
70+
close(serverPort);
71+
}
72+
});
73+
74+
it("should skip signature verification when skipSignatureVerification returns true", async () => {
75+
serverPort = TEST_PORT + 1;
76+
const m = middleware({
77+
channelSecret: "test_channel_secret",
78+
skipSignatureVerification: () => true,
79+
});
80+
await listen(serverPort, m);
81+
82+
const client = createClient(serverPort);
83+
84+
await client.post("/webhook", {
85+
events: [webhook],
86+
destination: DESTINATION,
87+
});
88+
89+
const req = getRecentReq();
90+
deepEqual(req.body.destination, DESTINATION);
91+
deepEqual(req.body.events, [webhook]);
92+
});
93+
94+
it("should skip signature verification when skipSignatureVerification returns false", async () => {
95+
serverPort = TEST_PORT + 2;
96+
const m = middleware({
97+
channelSecret: "test_channel_secret",
98+
skipSignatureVerification: () => false,
99+
});
100+
await listen(serverPort, m);
101+
102+
const client = createClient(serverPort);
103+
104+
try {
105+
await client.post("/webhook", {
106+
events: [webhook],
107+
destination: DESTINATION,
108+
});
109+
ok(false, "Expected to throw an error due to invalid signature");
110+
} catch (err) {
111+
if (err instanceof HTTPError) {
112+
equal(err.statusCode, 401);
113+
} else {
114+
throw err;
115+
}
116+
}
117+
});
118+
});
119+
56120
afterAll(() => {
57-
close();
121+
close(TEST_PORT);
58122
});
59123

60124
describe("Succeeds on parsing valid request", () => {

0 commit comments

Comments
 (0)