Using Secret Code

Note

  • For security reasons, it is highly recommended to use https for the webhook endpoint URL. https is more secure than http, and if https is used, the SSL/TLS certificate must be validated. (Self-signed certificates will not work.)
  • Currently, there are no designated IP addresses for Moaform webhook requests. Moaform is hosted on AWS servers, which use dynamic IP addresses, making it impossible to guarantee a static IP address or a range of IP addresses.

Since the endpoint URL for receiving webhooks can be exposed to the outside, it may be vulnerable to security threats. This issue can be mitigated by entering a 'Secret Code' in the webhook settings and having each webhook payload signed with this secret. The signature is included in the request header, allowing the verification of the webhook's origin from Moaform.

The webhook secret can be set when creating a webhook using the user interface, as well as when creating a new webhook via the API.

 

Validating Payload from Moaform

To validate the signature received in the payload from Moaform, you need to generate a signature using the secret code entered during webhook setup and compare it with the signature received in the webhook payload.

The method to generate a signature using the secret code is as follows:

  • Use the HMAC SHA-256 algorithm to create a hash (using the secret as the key) of the entire received payload in binary form.
  • Encode the binary hash in base64 format.
  • Add the prefix sha256= to the binary hash.
  • Compare the generated value with the signature received in the moaform-signature header from Moaform.

Below are examples in various programming languages.

 

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import org.apache.commons.lang3.StringUtils; // For secure string comparison

public class WebhookHandler {

    private static final String SECRET_TOKEN = System.getenv("SECRET_TOKEN"); // Retrieve from environment

    public void handleWebhook(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            String payloadBody = request.getReader().lines().collect(Collectors.joining());
            verifySignature(request.getHeader("moaform-signature"), payloadBody);
            response.getWriter().println("Payload received: " + payloadBody);
        } catch (SignatureMismatchException e) {
            response.sendError(500, "Signatures don't match!");
        }
    }

    private void verifySignature(String receivedSignature, String payloadBody) throws SignatureMismatchException {
        try {
            String actualSignature = calculateSignature(payloadBody);
            if (!StringUtils.equals(actualSignature, receivedSignature)) { // Secure comparison
                throw new SignatureMismatchException();
            }
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("Error calculating signature", e);
        }
    }

    private String calculateSignature(String payloadBody) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(SECRET_TOKEN.getBytes(), "HmacSHA256");
        hmac.init(secretKey);
        byte[] hash = hmac.doFinal(payloadBody.getBytes());
        return "sha256=" + Base64.getEncoder().encodeToString(hash);
    }
}

class SignatureMismatchException extends Exception {
}

 

Ruby

post '/webhook' do
  request.body.rewind
  payload_body = request.body.read
  verify_signature(request.env['moaform-signature'], payload_body)
  "Payload received: #{payload_body.inspect}"
end

def verify_signature(received_signature, payload_body)
  hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
  actual_signature = 'sha256=' + Base64.strict_encode64(hash)
  return halt 500, "Signatures don't match!" unless Rack::Utils.secure_compare(actual_signature, received_signature)
end

 

Node.js with Express

const crypto = require('crypto')
app.use(express.raw({ type: 'application/json' }))
app.post('/webhook', async (request, response) => {
  const signature = request.headers['moaform-signature']
  const isValid = verifySignature(signature, request.body.toString())
})

const verifySignature = function (receivedSignature, payload) {
  const hash = crypto
    .createHmac('sha256', process.env.SECRET_TOKEN)
    .update(payload)
    .digest('base64')
  return receivedSignature === `sha256=${hash}`}

 

Python

from fastapi import FastAPI,Request,HTTPException

import hashlib
import hmac
import json
import base64
import os

app = FastAPI()

@app.post("/hook")
async def recWebHook(req: Request):
  body = await req.json()
  raw = await req.body()

  receivedSignature = req.headers.get("moaform-signature")

  if receivedSignature is None:
    return HTTPException(403, detail="Permission denied.")

  sha_name, signature = receivedSignature.split('=', 1)
  if sha_name != 'sha256':
    return HTTPException(501, detail="Operation not supported.")

  is_valid = verifySignature(signature, raw)

  if(is_valid != True):
    return HTTPException(403, detail="Invalid signature. Permission Denied.")


def verifySignature(receivedSignature: str, payload):
  WEBHOOK_SECRET = os.environ.get('MOAFORM_SECRET_KEY')
  digest = hmac.new(WEBHOOK_SECRET.encode('utf-8'), payload, hashlib.sha256).digest()
  e = base64.b64encode(digest).decode()
  if(e == receivedSignature):
    return True
  return False