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 Affirm 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 recommended Tools to get started.
- For handling webhook over HTTPS, we recommend using this tool more specifically.
- You can find a working webhook signature code example on our codepen space.
/*
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 body.
We use the CryptoJS library for encryption in this example. Please find the appropriate 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 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 + "." + 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).
For checkout events, the requests sent from Affirm to your webhook endpoint come with the
content-type
application/x-www-form-urlencoded
and aapplication/x-www-form-urlencoded
version of the data in the body field. Prequal events are sent with thecontent-type
application/json
and aJSON
version of the data in the body field.
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.
Updated about 2 months ago