Securing Webhooks

After you configure webhooks (see Creating Webhooks) and your server to receive those request, you may want to verify that webhook requests come from Wix Answers.

Set Webhook Secret or Ensure You Have One

 TODO: wait for UI. than show to create secret

Validate Signature

When your webhook secret is set, Wix Answers uses it to create a hash signature with request payload.

This hash signature is passed along with each webhook request in the headers as 'X-Answers-Signature'.
The goal is to compute a hash using your SECRET, and ensure that the hash from Wix Answers matches. Wix Answers uses an HMAC-SHA256 algorithm to compute the hash. 
You could use the following code samples to see how to validate a signature:

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
var express = require('express');
var app = express();

var bodyParser = require('body-parser');
app.use(bodyParser.json());

var http = require("http");
var url = require("url");
var crypto = require("crypto");
var moment = require('moment');

var sharedSecret = "YOUR-SHARED-SECRET";

const validateSignature = (retrievedSignature, body) => {
    // Recalculate signature.
    var computedSignature = crypto.createHmac("SHA256", sharedSecret).update(body).digest("base64");
    console.log("computedSignature " + computedSignature);
    console.log("retrievedSignature " + retrievedSignature);
    // Compare signatures.
    const computed = Buffer.from(computedSignature, 'base64');
    const retrieved = Buffer.from(retrievedSignature, 'base64');
    const valid = crypto.timingSafeEqual(computed, retrieved);
    return valid;
}

const validateTimestamp = (timestamp) => {
    currentTimestamp = moment().valueOf();
    secondsInterval = Math.round((currentTimestamp - timestamp) / 1000);
    console.log('seconds interval: ' + secondsInterval);
    return secondsInterval < 1000000;
}

app.post('/', function(req, res){
    var body = JSON.stringify(req.body);
    // Get signature.
    var retrievedSignature = req.get('X-Answers-Signature');

    const isValidSignature = validateSignature(retrievedSignature, body);
    const isValidTimestamp = validateTimestamp(req.body.timestamp);
    if (isValidSignature && isValidTimestamp) {
        res.writeHead(200, { "content-type": "text/plain" });
        res.end("Hello World");
    } else {
        res.writeHead(403, { "content-type": "text/plain" });
        res.end("NOPE!");
    }
});

port = 1337;
app.listen(port);
console.log('Listening at http://localhost:' + port)

Python 2 Example:

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
#python

import base64
import hmac
import hashlib
import time

from flask import Flask, abort, request 
import json

app = Flask(__name__)
secret ='YOUR-SHARED-SECRET'

def validate_signature(body):
 signature = request.headers['X-Answers-Signature']
 print('signature: [' + signature + ']')
 digest = hmac.new(secret, body, hashlib.sha256).digest()
 computed_signature = base64.encodestring(digest).strip('\n').strip('\t')
 print('computed_signature: [' + computed_signature + ']')
 # NOTICE - Simple string comparisons not secure against timing attacks!!
 return computed_signature == signature

def validate_timestamp(body):
 json_body = json.loads(body)
 timestamp = json_body['timestamp']
 current_timestamp = int(round(time.time() * 1000))
 seconds_interval = (current_timestamp - timestamp) / 1000
 print('seconds interval: ' + str(seconds_interval))
 # define the desired second interval to prevent repeat attacks
 return seconds_interval < 10

@app.route('/', methods=['POST']) 
def foo():
 body = request.data
 is_valid_timestamp = validate_timestamp(body)
 is_valid_signature = validate_signature(body)
 return json.dumps(is_valid_signature and is_valid_timestamp)


if __name__ == '__main__':
 app.run(host='0.0.0.0', port=1337, debug=False)



Java Example:

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
import org.apache.commons.io.IOUtils;
import org.apache.commons.net.util.Base64;
import org.joda.time.DateTimeUtils;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class Validator {

    public static final String SECRET = "YOUR-SECRET-HERE";
    public static final String SIGNATURE_HEADER = "X-Answers-Signature";
    private final static int WEBHOOK_TTL_MILLIS = 10 * 1000; //allows request sent in the last 10 seconds

    
    public void validate(HttpServletRequest request) throws ValidationException, IOException {
        String sig = request.getHeader(SIGNATURE_HEADER);
        String body = IOUtils.toString(request.getReader());
        long timestamp = extractTimestamp(body)

        if (!isValidWebhookTTL(dto.getTimestamp())) {
            throw new ValidationException();
        }

        if (!isValidSignature(requestSignature, SECRET, body)) {
            throw new ValidationException();
        }
    }

    private boolean isValidSignature(String sig, String body) {
        Mac mac = Mac.getInstance(signAlgorithm);
            mac.init(new SecretKeySpec(SECRET.getBytes(), "HMACSHA256"));
              
        byte[] value = mac.doFinal(body.getBytes("UTF-8"))
        Base64 base64 = new Base64(256, (byte[])null, false);
        String signature = String(base64.encode(value));
        // TODO: not secure against timing attacks!!
        return signature == requestSignature;
    }

    private boolean isValidWebhookTTL(long timestamp) {
        long currentTimestamp = DateTimeUtils.currentTimeMillis();
        return currentTimestamp - timestamp <= WEBHOOK_TTL_MILLIS;
    }

}