From be8ad00c966077c081ec29bc1cb3881a3e528f06 Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Sat, 16 May 2026 13:19:06 +0530 Subject: [PATCH] feat(receivers): add invalidRequestSignatureHandler to HTTP/Express receivers Adds an optional callback that fires when request signature verification fails. This mirrors the existing behavior in AwsLambdaReceiver, letting developers add custom logging, metrics, or alerting when invalid signatures are detected. Fixes #2156 --- src/index.ts | 1 + src/receivers/ExpressReceiver.ts | 16 +++++++++++++++- src/receivers/HTTPModuleFunctions.ts | 20 ++++++++++++++++++++ src/receivers/HTTPReceiver.ts | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 52f1693dd..5cb114eda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ export type { ExpressReceiverOptions } from './receivers/ExpressReceiver'; export * as HTTPModuleFunctions from './receivers/HTTPModuleFunctions'; export { ReceiverDispatchErrorHandlerArgs, + ReceiverInvalidRequestSignatureHandlerArgs, ReceiverProcessEventErrorHandlerArgs, ReceiverUnhandledRequestHandlerArgs, RequestVerificationOptions, diff --git a/src/receivers/ExpressReceiver.ts b/src/receivers/ExpressReceiver.ts index e0a651ccf..33ebea73f 100644 --- a/src/receivers/ExpressReceiver.ts +++ b/src/receivers/ExpressReceiver.ts @@ -116,6 +116,7 @@ export interface ExpressReceiverOptions { // we can add a different name function for it. unhandledRequestHandler?: (args: httpFunc.ReceiverUnhandledRequestHandlerArgs) => void; unhandledRequestTimeoutMillis?: number; + invalidRequestSignatureHandler?: (args: httpFunc.ReceiverInvalidRequestSignatureHandlerArgs) => void; } // Additional Installer Options @@ -192,6 +193,7 @@ export default class ExpressReceiver implements Receiver { processEventErrorHandler = httpFunc.defaultProcessEventErrorHandler, unhandledRequestHandler = httpFunc.defaultUnhandledRequestHandler, unhandledRequestTimeoutMillis = 3001, + invalidRequestSignatureHandler = httpFunc.defaultInvalidRequestSignatureHandler, }: ExpressReceiverOptions) { this.app = app !== undefined ? app : express(); @@ -204,7 +206,7 @@ export default class ExpressReceiver implements Receiver { this.signatureVerification = signatureVerification; const bodyParser = this.signatureVerification - ? buildVerificationBodyParserMiddleware(this.logger, signingSecret) + ? buildVerificationBodyParserMiddleware(this.logger, signingSecret, invalidRequestSignatureHandler) : buildBodyParserMiddleware(this.logger); const expressMiddleware: RequestHandler[] = [ bodyParser, @@ -455,6 +457,7 @@ export function verifySignatureAndParseRawBody( function buildVerificationBodyParserMiddleware( logger: Logger, signingSecret: string | (() => PromiseLike), + invalidRequestSignatureHandler?: (args: httpFunc.ReceiverInvalidRequestSignatureHandlerArgs) => void, ): RequestHandler { return async (req, res, next): Promise => { // *** Parsing body *** @@ -474,6 +477,17 @@ function buildVerificationBodyParserMiddleware( if (error) { if (error instanceof ReceiverAuthenticityError) { logError(logger, 'Request verification failed', error); + if (invalidRequestSignatureHandler) { + const signature = req.headers['x-slack-signature']; + const ts = req.headers['x-slack-request-timestamp']; + invalidRequestSignatureHandler({ + signature: Array.isArray(signature) ? signature[0] : signature, + ts: ts ? Number(Array.isArray(ts) ? ts[0] : ts) : undefined, + body: stringBody, + request: req, + response: res, + }); + } res.status(401).send(); return; } diff --git a/src/receivers/HTTPModuleFunctions.ts b/src/receivers/HTTPModuleFunctions.ts index 7091b71fd..b2c2f1742 100644 --- a/src/receivers/HTTPModuleFunctions.ts +++ b/src/receivers/HTTPModuleFunctions.ts @@ -263,3 +263,23 @@ export interface ReceiverUnhandledRequestHandlerArgs { request: IncomingMessage; response: ServerResponse; } + +// The arguments for the invalidRequestSignatureHandler, +// which handles requests with invalid signatures. +export interface ReceiverInvalidRequestSignatureHandlerArgs { + signature: string | undefined; + ts: number | undefined; + body: string; + request: IncomingMessage; + response: ServerResponse; +} + +// The default invalidRequestSignatureHandler implementation: +// Developers can customize this behavior by passing invalidRequestSignatureHandler to the constructor +export const defaultInvalidRequestSignatureHandler = (args: ReceiverInvalidRequestSignatureHandlerArgs): void => { + const { signature, ts } = args; + // Intentionally does nothing by default. The receiver already responds with 401. + // This hook allows developers to add custom logging, metrics, or alerting. + void signature; + void ts; +}; diff --git a/src/receivers/HTTPReceiver.ts b/src/receivers/HTTPReceiver.ts index 115554707..45c5bd336 100644 --- a/src/receivers/HTTPReceiver.ts +++ b/src/receivers/HTTPReceiver.ts @@ -96,6 +96,7 @@ export interface HTTPReceiverOptions { // NOTE: As we use setTimeout under the hood, this cannot be async unhandledRequestHandler?: (args: httpFunc.ReceiverUnhandledRequestHandlerArgs) => void; unhandledRequestTimeoutMillis?: number; + invalidRequestSignatureHandler?: (args: httpFunc.ReceiverInvalidRequestSignatureHandlerArgs) => void; } // All the available argument for OAuth flow enabled apps @@ -169,6 +170,8 @@ export default class HTTPReceiver implements Receiver { private unhandledRequestTimeoutMillis: number; + private invalidRequestSignatureHandler: (args: httpFunc.ReceiverInvalidRequestSignatureHandlerArgs) => void; + public constructor({ signingSecret = '', endpoints = ['/slack/events'], @@ -190,6 +193,7 @@ export default class HTTPReceiver implements Receiver { processEventErrorHandler = httpFunc.defaultProcessEventErrorHandler, unhandledRequestHandler = httpFunc.defaultUnhandledRequestHandler, unhandledRequestTimeoutMillis = 3001, + invalidRequestSignatureHandler = httpFunc.defaultInvalidRequestSignatureHandler, }: HTTPReceiverOptions) { // Initialize instance variables, substituting defaults for each value this.signingSecret = signingSecret; @@ -254,6 +258,7 @@ export default class HTTPReceiver implements Receiver { this.processEventErrorHandler = processEventErrorHandler; this.unhandledRequestHandler = unhandledRequestHandler; this.unhandledRequestTimeoutMillis = unhandledRequestTimeoutMillis; + this.invalidRequestSignatureHandler = invalidRequestSignatureHandler; // Assign the requestListener property by binding the unboundRequestListener to this instance this.requestListener = this.unboundRequestListener.bind(this); @@ -448,6 +453,15 @@ export default class HTTPReceiver implements Receiver { const e = err as Error; if (this.signatureVerification) { this.logger.warn(`Failed to parse and verify the request data: ${e.message}`); + const signature = req.headers['x-slack-signature']; + const ts = req.headers['x-slack-request-timestamp']; + this.invalidRequestSignatureHandler({ + signature: Array.isArray(signature) ? signature[0] : signature, + ts: ts ? Number(Array.isArray(ts) ? ts[0] : ts) : undefined, + body: '', + request: req, + response: res, + }); } else { this.logger.warn(`Failed to parse the request body: ${e.message}`); }