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