@seratch_/bolt-fastify
Version:
Bolt for JavaScript Extension - Fastify
99 lines • 5 kB
JavaScript
;
/**
* 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