Skip to main content

Webhook signature verification

After confirming that the webhook endpoint is working as expected, secure the connection by implementing webhook signature verification.

This is especially important to verify that the webhook request was generated by Fliqa, and that it did not originate from a server impersonating Fliqa.

Signature

Each webhook POST request contains an X-Fliqa-Signature header in the following format:

t=1696242888,v=1e49bf8db353a19c3924822601452a7aacfd4f24487b7d4568bc9c5f9ac7386b

Alternatively, if a secret was regenerated within the last 24 hours:

t=1696242888,v=1e49bf8db353a19c3924822601452a7aacfd4f24487b7d4568bc9c5f9ac7386b,v0=f8f9c3507c01c2582241ca1b70c93aebea808757be800afbf374aeb402d4a101

where:

  • t = epoch timestamp in seconds: when the webhook signature was created to prevent replay attacks
  • v = the signature calculated from the secret, timestamp, webhook URL, and request body (JSON payload)
  • v0 = the signature calculated using the previous secret, if the secret was regenerated within the last 24 hours

The signature is calculated as follows:

  • the timestamp, webhook URL, and JSON request body are combined into a single string: timestamp.webhookUrl.body
  • the string is signed using the webhook secret and the HmacSHA256 algorithm
  • the result is encoded as a hexadecimal string
info

Use this online tool to check your algorithm matches ours!

Verification example

timestamp = 1698224457
URL = https://my.server.url/webhook
body = {"paymentId":"00000000000000000000000000000000","status":"successful","created":"2023-10-25T08:59:27.327069Z","modified":"2023-10-25T09:00:57.327093Z","paymentData":null,"providerId":"hooked-bank","name":"Test webhook call","description":"Manual webhook trigger","pointOfSaleId":"fb6e0508e9714066b45ea598acfe2df4","sourceIban":"SI56010000000100090","sourceName":"Janez Novak","targetIban":"SI56044030255412331","targetName":"Fliqa top up","amount":1.23,"currency":"EUR","country":"SI","data":[{"key":"customer_id","value":"0000-00-0000"}],"locale":"en"}

secret = 0ddf43e8-43fa-46ce-8bb0-c6aab3c0b511

Should produce the following signature:

t=1698224457,
v=0a492fc70a2bf572e9eb05e66f8e490200ad6a68809d5501e23511efaf1814de

Verification implementation examples

/**
See full implementation example at:
https://github.com/fliqa-io/examples/blob/main/java/src/main/java/io/fliqa/example/webhook/WebHookUtils.java
**/

package io.fliqa.example.webhook;

public class WebHookUtils {

private static final String DIGEST = "HmacSHA256";

public static boolean checkSignature(String signature, String secret, String oldSecret, String hookUrl, String body) {

// Split signature to t={time},v={verification},v0={old_verification}
String[] timeAndSignature = signature.split(",");
if (timeAndSignature.length < 2 || timeAndSignature.length > 3) {
throw new IllegalArgumentException(
String.format("Invalid signature, expected time and verification but got: '%s'!", signature)
);
}

Long time = getSignatureTime(timeAndSignature);

String oldVerification = null;
String oldCompare = null;
if (timeAndSignature.length == 3) { // there is the old signature present (double check)
oldVerification = getOldSignatureVerification(timeAndSignature);
oldCompare = sign(oldSecret, time.toString(), hookUrl, body);
}

String verification = getSignatureVerification(timeAndSignature);
String compare = sign(secret, time.toString(), hookUrl, body);

return verification.equals(compare)
|| (oldVerification != null && oldVerification.equals(oldCompare));
}

protected static String sign(String secret, String time, String hookUrl, String content) {
String input = String.format("%s.%s.%s", time, hookUrl, content);

try {
Mac mac = Mac.getInstance(DIGEST);
mac.init(new SecretKeySpec(secret.getBytes(), DIGEST));
return toHexString(mac.doFinal(input.getBytes()));
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
// Failed to create SHA signature!
return "";
}
}

public static String toHexString(byte[] arg) {
return String.format("%x", new BigInteger(1, arg));
}
}