-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathvalidateHeapHeader.ts
75 lines (69 loc) · 2.7 KB
/
validateHeapHeader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/* eslint-disable @typescript-eslint/no-explicit-any */
require('dotenv').config();
import { Context } from 'koa';
import * as CryptoJS from 'crypto-js';
import {
HEAP_TIMESTAMP_KEY,
HEAP_HMAC_KEY,
MAX_OLD_TIMESTAMP_DELTA,
MAX_FUTURE_TIMESTAMP_DELTA,
} from '../constants';
// parse the heap-hash header, which has the format `ts:${timestamp},hmac:${hmac}`
const extractTimeStampAndHMAC = (heapHeader: string, ctx: Context): Map<string, any> => {
const heapMap: Map<string, any> = new Map();
try {
const tsSegment = heapHeader.split(',')[0];
const heapSegment = heapHeader.split(',')[1];
if (tsSegment.split(':')[0] === HEAP_TIMESTAMP_KEY)
heapMap.set('ts', parseInt(tsSegment.split(':')[1]));
if (heapSegment.split(':')[0] === HEAP_HMAC_KEY) heapMap.set('hmac', heapSegment.split(':')[1]);
} catch (e) {
ctx.throw(400, 'Improperly formatted "heap-hash" header');
}
if (!heapMap.has('ts')) {
ctx.throw(400, 'Expected timestamp in "heap-hash" header');
}
if (!heapMap.has('hmac')) {
ctx.throw(400, 'Expected hmac in "heap-hash"');
}
return heapMap;
};
// validate that the timestamp provided in the header is less than
// MAX_OLD_TIMESTAMP_DELTA (1 minute) old
const isTimeStampWithinThreshold = (ts: number): boolean => {
const now = +Date.now();
if (now - ts > MAX_OLD_TIMESTAMP_DELTA) {
return false;
}
return ts - now < MAX_FUTURE_TIMESTAMP_DELTA;
};
// Compute the hmac using your WEBHOOK_SECRET and the timestamp concatenated with the request body,
// then verify that it matches the provided hmac
export const validateHeapHeader = async (ctx: Context, next: () => Promise<any>): Promise<any> => {
if (ctx.path !== '/users_sync' && ctx.path !== '/users_drain') {
return next()
}
const heapHeader = ctx.request.headers['heap-hash'] as string;
if (!heapHeader) {
ctx.throw(400, 'Expected "heap-hash" in the header');
}
if (!process.env.WEBHOOK_SECRET) {
ctx.throw(500, 'Webhook Secret Key has not been configured');
}
const heapMap: Map<string, any> = extractTimeStampAndHMAC(heapHeader, ctx);
if (!isTimeStampWithinThreshold(heapMap.get(HEAP_TIMESTAMP_KEY))) {
ctx.throw(400, 'Timestamp of "heap-hash" is not within threshold');
}
// The hmac will use the timestamp + data concatenation for the base, and
// the shared webhook secret key as the key.
const hmac = CryptoJS.HmacSHA256(
`${heapMap.get('ts')}${JSON.stringify(ctx.request.body)}`,
process.env.WEBHOOK_SECRET,
);
const computedHmac = CryptoJS.enc.Base64.stringify(hmac);
const receivedHmac = heapMap.get('hmac');
if (computedHmac !== receivedHmac) {
ctx.throw(403, `Invalid hmac. Recieved: ${receivedHmac}, Computed: ${computedHmac}`);
}
await next();
};