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. First ensure that you have the webhook secret.

Get Webhook Secret

GET https://<tenant_subdomain>.wixanswers.com/api/v1/settings/account/webhooks/secret/reveal

Only the account owner (the administrator who created tenant) can obtain the webhook secret. At any time there is a primary secret and a secondary secret. Use the primary secret. If the primary secret is compromised, rotate the secrets to generate a new primary secret; see Rotate Webhook Secret). This endpoint is typically only accessed using the Wix Answers app.

  • Authorization: Account owner, only, with correct browser cookie (token not accepted)
  • Content type: application/json; charset=utf-8.
  • Accept: application/json.
  • Response: Structure (see below)
Request Example:
1
GET https://<tenant_subdomain>.wixanswers.com/api/v1/settings/account/webhooks/secret/reveal
Response:
Response Params
Description
Type
primarySecret
The secret
String
secondarySecret
This becomes the new primary secret if you rotate the secrets.
String
1
2
3
4
{
 "primarySecret": "<secret>",
 "secondarySecret": "<secret>"
}

Rotate Webhook Secret

GET https://<tenant_subdomain>.wixanswers.com/api/v1/settings/account/webhooks/secret/rotate

Only the account owner (the administrator who created tenant) can rotate the webhook secret. If the primary secret is compromised, rotate the secrets to generate a new primary secret: the secondary secret becomes the new primary secret, and a new secondary secret is generated. This endpoint is typically only accessed using the Wix Answers app.

  • Authorization: Account owner, only, with correct browser cookie (token not accepted)
  • Content type: application/json; charset=utf-8.
  • Accept: application/json.
  • Response: Structure (same as for Get Webhook Secret, see above)
Request Example:
1
GET https://<tenant_subdomain>.wixanswers.com/api/v1/settings/account/webhooks/secret/rotate

Validate Signature

When your webhook secret is set, Wix Answers uses it to create a hash signature with the request payload. This hash signature is passed along with each webhook request in the headers, as X-Answers-Signature.

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.

The following are examples of how to validate a signature:

JavaScript Example:

bodyParser verification fail on emojis
In the following example, if you use app.use(bodyParser.json()), the verification fails if the payload contains emojis (due to differences in encoding).
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
var express = require('express');
var app = express();

var bodyParser = require('body-parser');
app.use(bodyParser.text({type:"*/*"}));

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 retrievedSignature = req.get('X-Answers-Signature');
    const isValidSignature = validateSignature(retrievedSignature, req.body);

    var timestamp = JSON.parse(req.body).timestamp;
    const isValidTimestamp = validateTimestamp(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;
    }

}