UNPKG

customerio-node

Version:

A node client for the Customer.io event API. http://customer.io

63 lines (62 loc) 2.54 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyRequestSignature = verifyRequestSignature; const crypto_1 = require("crypto"); const utils_1 = require("./utils"); /** Version prefix Customer.io includes when signing webhook requests. Always `v0`. */ const SIGNATURE_VERSION = 'v0'; /** * Verifies that a webhook request genuinely originated from Customer.io. * * Customer.io signs each webhook by computing an HMAC-SHA256 of the string * `v0:<timestamp>:<body>` (keyed with your webhook signing secret) and sends the * hex-encoded result in the `X-CIO-Signature` header, alongside the * `X-CIO-Timestamp` header. This recomputes that signature and compares it to * the header value in constant time. * * @returns `true` if the signature is valid, `false` otherwise. A malformed or * missing `signature` returns `false` rather than throwing. * @throws {MissingParamError} if `signingSecret` is empty. * * @see https://docs.customer.io/api/webhooks/ * * @example * ```ts * import { verifyRequestSignature } from 'customerio-node'; * * const valid = verifyRequestSignature({ * signingSecret: process.env.CIO_WEBHOOK_SECRET, * timestamp: req.headers['x-cio-timestamp'], * signature: req.headers['x-cio-signature'], * body: req.rawBody, // the raw request body, not the parsed object * }); * * if (!valid) { * res.status(400).send('invalid signature'); * return; * } * ``` */ function verifyRequestSignature({ signingSecret, timestamp, body, signature, }) { if (!signingSecret) { throw new utils_1.MissingParamError('signingSecret'); } // Header values may be absent or, in Node, arrive as `string[]` when a header // is repeated. Anything that isn't a non-empty string can't be a valid hex // signature, so reject it before hashing. if (typeof signature !== 'string' || signature.length === 0) { return false; } const hmac = (0, crypto_1.createHmac)('sha256', signingSecret); hmac.update(`${SIGNATURE_VERSION}:${timestamp}:`); hmac.update(body); const expected = hmac.digest('hex'); const expectedBuffer = Buffer.from(expected, 'utf8'); const providedBuffer = Buffer.from(signature, 'utf8'); // `timingSafeEqual` throws on length mismatch; the length of a hex digest is // not secret, so compare it directly first. if (expectedBuffer.length !== providedBuffer.length) { return false; } return (0, crypto_1.timingSafeEqual)(expectedBuffer, providedBuffer); }