UNPKG

@seratch_/bolt-fastify

Version:
99 lines 5 kB
"use strict"; /** * Functions used to verify the authenticity of incoming HTTP requests from Slack. * * The functions in this file are intentionally generic (don't depend on any particular web framework) and * time-independent (for testing) so they can be used in a wide variety of applications. The intention is to distribute * these functions in its own package. * * For now, there is some duplication between the contents of this file and ExpressReceiver.ts. Later, the duplication * can be reduced by implementing the equivalent functionality in terms of the functions in this file. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseAndVerifyRequest = void 0; const crypto_1 = require("crypto"); const raw_body_1 = __importDefault(require("raw-body")); const tsscmp_1 = __importDefault(require("tsscmp")); const verifyErrorPrefix = 'Failed to verify authenticity'; /** * Verify the authenticity of an incoming HTTP request from Slack and buffer the HTTP body. * * When verification succeeds, the returned promise is resolved. When verification fails, the returned promise is * rejected with an error describing the reason. IMPORTANT: The error messages may contain sensitive information about * failures, do not return the error message text to users in a production environment. It's recommended to catch all * errors and return an opaque failure (HTTP status code 401, no body). * * Verification requires consuming `req` as a Readable stream. * If the `req` was consumed before this function is called, * then this function expects it to be stored as a Buffer at `req.rawBody`. This is a convention used by infrastructure * platforms such as Google Cloud Platform. When the function returns, the buffered body is stored at the `req.rawBody` * property for further handling. * * The function is designed to be curry-able for use as a standard http RequestListener, and therefore keeps `req` and * `res` are the last arguments. However, the function is also async, which means when it is curried for use as a * RequestListener, the caller should also capture and use the return value. */ async function parseAndVerifyRequest(options, req, _res) { var _a; const { signingSecret } = options; // Consume the readable stream (or use the previously consumed readable stream) const bufferedReq = await bufferIncomingMessage(req); if (options.enabled !== undefined && !options.enabled) { // As the validation is disabled, immediately return the bufferred reuest return bufferedReq; } // Find the relevant request headers const signature = getHeader(req, 'x-slack-signature'); const requestTimestampSec = Number(getHeader(req, 'x-slack-request-timestamp')); if (Number.isNaN(requestTimestampSec)) { throw new Error(`${verifyErrorPrefix}: header x-slack-request-timestamp did not have the expected type (${requestTimestampSec})`); } // Calculate time-dependent values const nowMsFn = (_a = options.nowMs) !== null && _a !== void 0 ? _a : (() => Date.now()); const nowMs = nowMsFn(); const fiveMinutesAgoSec = Math.floor(nowMs / 1000) - 60 * 5; // Enforce verification rules // Rule 1: Check staleness if (requestTimestampSec < fiveMinutesAgoSec) { throw new Error(`${verifyErrorPrefix}: stale`); } // Rule 2: Check signature // Separate parts of signature const [signatureVersion, signatureHash] = signature.split('='); // Only handle known versions if (signatureVersion !== 'v0') { throw new Error(`${verifyErrorPrefix}: unknown signature version`); } // Compute our own signature hash const hmac = (0, crypto_1.createHmac)('sha256', signingSecret); hmac.update(`${signatureVersion}:${requestTimestampSec}:${bufferedReq.rawBody.toString()}`); const ourSignatureHash = hmac.digest('hex'); if (!(0, tsscmp_1.default)(signatureHash, ourSignatureHash)) { throw new Error(`${verifyErrorPrefix}: signature mismatch`); } // Checks have passed! Return the value that has a side effect (the buffered request) return bufferedReq; } exports.parseAndVerifyRequest = parseAndVerifyRequest; async function bufferIncomingMessage(req) { if (isBufferedIncomingMessage(req)) { return req; } const bufferedRequest = req; bufferedRequest.rawBody = await (0, raw_body_1.default)(req); return bufferedRequest; } function isBufferedIncomingMessage(req) { return Buffer.isBuffer(req.rawBody); } function getHeader(req, header) { const value = req.headers[header]; if (value === undefined || Array.isArray(value)) { throw new Error(`${verifyErrorPrefix}: header ${header} did not have the expected type (${value})`); } return value; } //# sourceMappingURL=verify-request.js.map