To verify that webhook requests originated from the Flamelink servers, as opposed to a third-party, and haven’t been tampered with, you can optionally enable webhook signing in your webhook configuration. Flamelink will then set a signature header, x-flamelink-signature for each request that you can use to verify it.

Preventing replay attacks

A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Flamelink includes a timestamp in the x-flamelink-signature header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your application reject the payload.

You can decide your tolerance period when verifying the signature. Normally, allowing for a period of 5 mins difference is a good start, but this might differ for your particular use case. We recommend that you use Network Time Protocol (NTP) to ensure that your server’s clock is accurate and synchronizes with the time on Flamelink's servers.

Flamelink generates the timestamp and signature each time we send a request to your endpoint. If Flamelink retries a request (e.g., your endpoint previously replied with a non-2xx status code), then we generate a new signature and timestamp for the new delivery attempt.

Verifying signatures manually

You can use the following instructions to verify the request signature:

The x-flamelink-signature  header contains a timestamp and a signature. The timestamp is prefixed by t=, and the signature is prefixed by s= .

x-flamelink-signature: t=1559801691997,s=128788aa77a3bcd306befcf3fab434feaf44dee7956711497b9c93cb6b986c81

We generate signatures using a hash-based message authentication code (HMAC) with SHA-256.

Step 1: Extract the timestamp and signatures from the header

Read and split the header using the , (comma) character as the separator, to get a list of elements. Then split each element, using the = (equal) character as the separator, to get a prefix and value pair.

The value for the prefix t  corresponds to the timestamp, and s  corresponds to the signature. You can discard all other elements.

Step 2: Prepare the signed_payload  string

You achieve this by concatenating:

  • The timestamp (as a string)
  • The .  (dot) character
  • The JSON-stringified request payload (i.e., the request’s body)

For the timestamp we use the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC.

Step 3: Determine the expected signature

Compute an HMAC  with the SHA256  hash function. Use the service account, that you provided to Flamelink’s, private_key as the key, and use the computed signed_payload  string as the message.

Step 4: Compare signatures

Compare the signature in the header to the expected signature (perform a simple string match). If the signature matches, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

JavaScript example using crypto-js

const CryptoJS = require('crypto-js');
const { differenceInMinutes } = require('date-fns');

const flamelinkSignature = // `x-flamelink-signature` header
const eventPayload = // request body received
const privateKey = // `private_key` from service account

const [timestampPair, signaturePair] = flamelinkSignature.split(',')
const [, timestamp] = timestampPair.split('=')
const [, signature] = signaturePair.split('=')

const signedPayload = ${timestamp}.${JSON.stringify(eventPayload)};
const testSignature = CryptoJS.HmacSHA256(signedPayload, privateKey);

if (testSignature === signature) {
  // request originated from Flamelink

  // Optionally check timestamp against current timestamp
  const TOLERANCE_MINUTES = 5;
  const currentDate = new Date();
  const compareDate = new Date(timestamp);

  if (differenceInMinutes(currentDate, compareDate) <= TOLERANCE_MINUTES) {
    // Request within tolerance difference
  }
}
 
Did this answer your question?