Security

Verify webhook authenticity and prevent tampering

Overview

To ensure that webhook requests originate from Fynapse and have not been tampered with, each request is signed using a shared secret.

Your system must validate this signature before processing any webhook payload.

Why Validation Is Required

Webhook endpoints are public HTTP endpoints. Without validation, an attacker could:

  • Send forged requests to your system
  • Replay previously captured webhook payloads
  • Trigger unintended processing

Signature verification ensures that:

  • the request was sent by Fynapse
  • the payload has not been modified in transit

Signing Model

Fynapse signs each webhook request using HMAC-SHA256.

The signature is calculated over the following input:

<timestamp>.<raw_request_body>

Where:

  • timestamp is a Unix timestamp (seconds)
  • raw_request_body is the exact HTTP request body as received

Signature Header Format

Each request includes a Webhook-Signature header:

Webhook-Signature: t=<timestamp>,v1=<signature>[,v1=<previous_signature>]

Components

t — Unix timestamp used in the signature v1 — HMAC-SHA256 signature (hex-encoded)

During secret rotation, multiple v1 values may be present. Your system should accept the request if any one of the signatures is valid.

How to Validate a Request

To verify a webhook request:

  • Extract the Webhook-Signature header
  • Parse the timestamp (t) and all v1 signatures
  • Reconstruct the signing input: <timestamp>.<raw_request_body>
  • Compute the HMAC-SHA256 using your secret
  • Compare the computed signature with each provided v1 value
  • Accept the request if any match

Validation Rules

When implementing validation, follow these rules:

RuleDescription
Use raw request bodyUse the body exactly as received; do not reformat or reserialize JSON
Perform constant-time comparisonPrevent timing attacks when comparing signatures
Validate timestamp freshnessReject requests outside an acceptable time window (e.g., 5 minutes)
Support multiple signaturesAllow validation against multiple signatures for secret rotation
Include timestamp in signed inputAlways include timestamp; never validate against the body alone

The following example demonstrates how to validate a webhook signature in Java:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class WebhookSignatureVerifier {
private static final String HMAC_SHA256 = "HmacSHA256";
private static final HexFormat HEX = HexFormat.of();
private static final Duration MAX_CLOCK_SKEW = Duration.ofMinutes(5);
public static boolean verify(String signatureHeader, String rawBody, String secret) {
if (signatureHeader == null || signatureHeader.isBlank()) {
return false;
}
if (rawBody == null || secret == null || secret.isBlank()) {
return false;
}
ParsedSignature parsed = parseHeader(signatureHeader);
long now = Instant.now().getEpochSecond();
if (Math.abs(now - parsed.timestamp()) > MAX_CLOCK_SKEW.toSeconds()) {
return false;
}
String signingInput = parsed.timestamp() + "." + rawBody;
String expected = hmacSha256Hex(signingInput, secret);
for (String provided: parsed.signatures()) {
if (MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), provided.getBytes(StandardCharsets.UTF_8))) {
return true;
}
}
return false;
}
private static String hmacSha256Hex(String value, String secret) {
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
return HEX.formatHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException("Cannot verify webhook signature", e);
}
}
private static ParsedSignature parseHeader(String header) {
Long timestamp = null;
List < String > signatures = new ArrayList < > ();
for (String part: header.split(",")) {
String[] keyValue = part.trim().split("=", 2);
if (keyValue.length != 2) {
continue;
}
if ("t".equals(keyValue[0])) {
timestamp = Long.parseLong(keyValue[1]);
} else if ("v1".equals(keyValue[0])) {
signatures.add(keyValue[1]);
}
}
if (timestamp == null || signatures.isEmpty()) {
throw new IllegalArgumentException("Invalid Webhook-Signature header");
}
return new ParsedSignature(timestamp, signatures);
}
private record ParsedSignature(long timestamp, List < String > signatures) {}
}

Secret Management

Creating a Secret

A webhook secret is generated by Fynapse and used to sign requests.

  • The secret value is returned only when:
    • creating a secret
    • rotating a secret
    • retrieving secret details
  • You must store this value securely on your side

Using a Secret

  • Each webhook subscription references a secret
  • The same secret is used to sign all deliveries for that subscription

Rotating a Secret

Secrets can be rotated to improve security.

During rotation:

  • Fynapse may include multiple signatures in the header
  • One corresponds to the new secret
  • One corresponds to the previous secret

This allows you to:

  • deploy the new secret safely
  • continue accepting requests during the transition