Message Security

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.