Checking webhook signatures

Verify the events that Affirm sends to your webhook endpoints.

Affirm signs the webhook events it sends to your endpoints. We do so by including a signature in each event’s Affirm-Signature header. This allows you to verify that the events were sent by Affirm, not by a third party. You can verify signatures manually using your own solution.

🚧

A webhook signature can be configured by Affirm only. Please reach out to us via the support widget.

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, Affirm includes a timestamp in the Affirm-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.

❗️

Timestamp tolerance

We recommend that you set a default tolerance (e.g five minutes) between the timestamp and the current time, and use Network Time Protocol (NTP) to ensure that your server’s clock is accurate and synchronizes with the time on Affirm’s servers.

Affirm generates the timestamp and signature each time we send an event to your endpoint. If Affirm retries an event (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

The X-Affirm-Signature header contains a timestamp and one or more signatures. The timestamp is prefixed by t=, and each signature is prefixed by a scheme. Schemes start with v, followed by an integer. Currently, the only valid signature scheme is v0.

x-affirm-signature:
t=1582267948,
v0=c5e024fb3c0d16efd329094f45fc8c1b00b89b6a648731f83a8f7115143ee9e123a0e19a8edd4be5d40274401dd6700b77dc4954efb8d93ac68611ad5d3e6446

You perform the verification by providing the webhook payload, the X-Affirm-Signature header, and the endpoint’s secret. If verification fails, you can return an error.

📘

Make sure to check our recommend Tools to get started.

/*
 Couple of things to set up. Those will also changes based on environment, production vs sandbox.
 - The Authorization header is not useful, and no used as the secret.
 - Get merchant private key. This is actually the secret used for encryption.
 - Get the x-affirm-signature header from the webhook.
 - Make sure to pick-up the raw_body.
 
 We use the CryptoJS library for encryption in this example. Please find the approriate library based on your programming language.
 
 Webhook signature validation supported:
 - x-affirm-signature
*/

// Can be found in you Affirm dashboard (prod vs sandbox)
let private_key = "A3aut6z2VemhGHPgYF6uBFqczAm4VyyJ";
// Can be found in the webhook payload header
let x_affirm_signature = "t=1597184450,v0=f22309810ee2fc8f7f0ff41e0b1ceb74de98b5077385882e8f93c5d0f5ff86684e38c45531b3d34f07d5dd13a2e7c2c44ddb71d4e67e9a0b781a5976d18e0d42";
// Can be found in the payload (e.g Affirm only supports XML) of the Webhook
let raw_body = "checkout_token=N8R79PUSKRP2UNAJ&created=2020-08-11T22%3A20%3A48.961423&email_address=john.doe%40affirm.com&event=opened&event_timestamp=2020-08-11T22%3A20%3A50.247581&total=60000";

const details = parseHeader(x_affirm_signature, "v0");

console.log(details);

if (!details || details.timestamp === -1) {
  try {
    throw new Error("Unable to extract timestamp and signature from header")
    console.log("Unable to extract timestamp and signature from header")
  } catch (e) {
    console.log(e.name, e.message);
  }
}

if (!details || details.signature === -1) {
   try {
    throw new Error("No signature found with expected scheme")
    console.log("No signature found with expected scheme")
  } catch (e) {
    console.log(e.name, e.message);
  }
}

// This is where the magic happens.
let payload = details.timestamp + "." + raw_body;
let expectedSignature = CryptoJS.HmacSHA512(payload, private_key).toString(CryptoJS.enc.Hex);

console.log(expectedSignature);

if(expectedSignature == details.signature) {
   console.log("Signature match with x-affirm-signature");
} 
    
function parseHeader(header, scheme) {
  if (typeof header !== 'string') {
    return null;
  }

  return header.split(',').reduce(
    (accum, item) => {
      const kv = item.split('=');

      if (kv[0] === 't') {
        accum.timestamp = kv[1];
      }

      if (kv[0] === scheme) {
        accum.signature = kv[1];
      }

      return accum;
    },
    {
      timestamp: -1,
      signature: -1,
    }
  );
}

Affirm generates signatures using a hash-based message authentication code (HMAC) with SHA-512. To prevent downgrade attacks, you should ignore all schemes that are not v0.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the, character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

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

Step 2: Prepare the signed_payload string

You achieve this by concatenating:

  • The timestamp, i.e. the value of "t" (as a string).
  • The character .
  • The actual raw payload (i.e., the request’s body)

📘

The raw payload, as described above, should utilize the form-urlencoded version of the request body. While the requests sent from Affirm to your webhook endpoint comes with the content-type application/x-www-form-urlencoded, the request gives both an application/x-www-form-urlencoded and JSON version of the data in the bodyRaw and body fields respectively.

Step 3: Determine the expected signature

Compute an HMAC with the SHA512 hash function. Use the endpoint’s signing secret (your private key) as the key, and use the signed_payload string as the message.

Step 4: Compare signatures

Compare the signature(s) in the header to the expected signature. If a 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.


Did this page help you?