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}`); }