Securing Webhook Requests

When creating a new webhook, we have the option to provide a secret key. This secret key is an optional field. Including a secret key ensures that the webhook is truly from Neeto and not from a hacker.

Setting up the secret key on the server

You must create an environment variable on the server to store the secret key.

Usually, this involves running the following command:

export SECRET_KEY=YOUR-SECRET-KEY

Validating requests are from your Neeto product

When your secret key is set, your Neeto product uses that secret key to create a hash signature with each payload. This hash signature is included with the headers of each request as x-neeto-webhook-signature.

You should calculate a hash using your SECRET_KEY and ensure that the result matches the hash from your Neeto product.

Your language and server implementations may differ from the following examples. However, there are a number of very important things to point out:

  • No matter which implementation you use, the hash signature starts with sha256=.

  • Using a plain== operator is not advised. A method like secure_compare performs a "constant time" string comparison, which helps mitigate certain timing attacks against regular equality operators.

Ruby example:

For example, you can define the following verify_signature function:

def verify_signature(payload_body)
  signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_KEY'], payload_body)
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_NEETO_CAL_SIGNATURE_256])
end

Then, you can call it when you receive a webhook payload:

post '/payload' do
  request.body.rewind
  payload_body = request.body.read
  verify_signature(payload_body)
  push = JSON.parse(payload_body)
  "I got some JSON: #{push.inspect}"
end

Python example:

For example, you can define the following verify_signature function and call it when you receive a webhook payload:

import hashlib
import hmac
def verify_signature(payload_body, secret_key, signature_header):
    """Verify that the payload was sent from your neeto product by validating SHA256.
    
    Raise and return 403 if not authorized.
    
    Args:
        payload_body: original request body to verify (request.body())
        secret_key: neeto product webhook secret key (WEBHOOK_SECRET)
        signature_header: header received from neeto product (x-neeto-webhook-signature)
    """
    if not signature_header:
        raise HTTPException(status_code=403, detail="x-neeto-webhook-signature header is missing!")
    hash_object = hmac.new(secret_key.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
    expected_signature = "sha256=" + hash_object.hexdigest()
    if not hmac.compare_digest(expected_signature, signature_header):
        raise HTTPException(status_code=403, detail="Request signatures didn't match!")