Each webhook can optionally define a secret cryptographic key in the HMAC SHA256 Secret field. FastSpring servers will use that key to generate a hashed digest of each webhook payload. The resulting digest will be encoded to base64 and included in the X-FS-Signature header of the webhook. Your server can then use the same process, creating a hashed digest of the payload using the same secret key on your side, and then encoding the resulting hash to base64 before comparing it to the value in that header.
A post with a valid, matching digest in the header can only have originated from a source that uses the correct secret key. If the key has been provided only to FastSpring, via the webhook interface in the FastSpring App (i.e., not used anywhere else), this confirms that the webhook data is authentic. You can find more information about hash-based message authentication at <https://en.wikipedia.org/wiki/Hash-based_message_authentication_code>.
The X-FS-Signature header sent by FastSpring is not case-sensitive and might be sent with varying case (all lowercase, or mixed case). We recommend capturing the incoming webhook data--including the header--for verification while adding/registering. The console will log the request and response so that the header contents can be inspected.
Validating message signatures
See the following example code snippets to help you validate message signatures
Java
/**
*
* @param request - Standard HttpServletRequest
* @param secret - The secret string saved in the FastSpring App, under webhooks
* @return true - Valid Request, trust request
* false - Invalid or spoofed, reject request
*/
public boolean isValid(HttpServletRequest request, String secret) throws Exception {
String fsSignature = request.getHeader("x-fs-signature");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKeySpec);
String calculatedSignature = Base64.getEncoder().encodeToString(mac.doFinal(request.getInputStream().readAllBytes()));
return calculatedSignature.equals(fsSignature);
}
Node.js with built-in html module
const http = require('http');
const crypto = require('crypto');
const secret = ""; // The secret string saved in the FastSpring App, under webhooks
const server = http.createServer((req, res) => {
let body = [];
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
// Load the Raw Body.
body = Buffer.concat(body).toString();
// Get Hashed Signature header
const fsSignature = req.headers['x-fs-signature'];
let valid = isValidSignature(body, fsSignature, secret);
res.statusCode = valid ? 200 : 400;
res.end(valid ? "OK" : "BAD REQUEST");
});
});
server.listen(3000, "127.0.0.1", () => {});
/**
* Validates a FastSpring webhook
*
* @param {string} body The Raw Body of the request.
* @param {string} fsSignature the 'x-fs-signature' header value
* @param {string} secret the secret string saved in the FastSpring App
*/
const isValidSignature = (body, fsSignature, secret) => {
const computedSignature = crypto.createHmac('sha256', secret)
.update(body)
.digest()
.toString('base64');
return fsSignature === computedSignature;
}
Node.js with Express framework
// (Warning when using express and json, you must valid before the json parser)
const express = require('express')
const crypto = require("crypto");
const app = express()
const secret = "";
// Setup validate function to validate before json parser
app.use(express.json({
verify: function(req, res, buf, encoding) {
const fsSignature = req.headers['x-fs-signature'];
let isValid = isValidSignature(buf.toString(), fsSignature, secret);
// Reject request if not valid
}}));
app.post('/', (req, res) => {
// Handle request
});
app.listen(3000, () => {});
const isValidSignature = (body, fsSignature, secret) => {
const computedSignature = crypto.createHmac('sha256', secret)
.update(body)
.digest()
.toString('base64');
return fsSignature === computedSignature;
}
PHP
Note: Nginx users need to enable underscores in headers to use this.
$hash = base64_encode( hash_hmac( 'sha256', file_get_contents('php://input') , $secret, true ) ); if ($hash == $_SERVER['X-Fs-Signature']) { /* Your code here */ }
IP Filtering
Another method of increasing confidence that webhooks are being sent by Fastspring is to compare the IP which sent the request. Webhooks delivered from API will come from 107.23.30.83. However, IP addresses can be spoofed so IP detection is not entirely secure.