Are you looking to make it easier to preview and QA Optimizely experiments? Download the OptiPilot Companion to easily see active experiments, preview events & more!

How to verify your Optimizely webhook signature (Step-by-step)

If you ever would like to be notified about datafile updates, you will want to leverage Optimizely’s webhook mechanism.

With it, Optimizely servers will send a ping to an URL of your choice letting you know the datafile has been updated.

From a security perspective, you would end up with a public endpoint accepting requests from anyone.

This means an attacker could DDos your endpoint…that’s where the webhook signature comes in.

With it, we can read a hash the Optimizely servers add to the request header to ensure it originates from Optimizely, for real; and not from some attacker.

Before you apply this tutorial, we’re going to assume you already have a webhook configured inside the Optimizely interface. If not, log in, to to Settings->Webhook and enter the URL of your choice.

The code to verify the webhook signature

Now, all we’ll do is:

  • Open an endpoint that waits for requests made by Optimizely servers
  • When there’s an incoming request, we’ll check the request signature against the webhook secret
  • We’ll apply SHA1 to both and confirm if they match
  • If they match, it means the request originated from Optimizely servers and is legit; then you can update the datafile accordingly.

To apply SHA1 hash, we need to read the raw payload, hence why we’re running a function called verifyPostData that will ensure we only consider the raw payload.

Notice also there’s a variable called secret which will need to be replaced with the webhook secret Optimizely provides upon creating the webhook endpoint in the Optimizely user interface.

Here’s the code below, that will do exactly that:

import express from "express";
import ViteExpress from "vite-express";

const app = express();
import bodyParser from 'body-parser';
import crypto from 'crypto'

const sigHeaderName = 'X-Hub-Signature'
const sigHashAlg = 'sha1'
const secret = "CHANGE_THIS"

app.use(bodyParser.json({
  verify: (req, res, buf, encoding) => {
    if (buf && buf.length) {
      req.rawBody = buf.toString(encoding || 'utf8');
    }
  },
}))

function verifyPostData(req, res, next) {
  if (!req.rawBody) {
    return next('Request body empty')
  }

  const sig = Buffer.from(req.get(sigHeaderName) || '', 'utf8')
  const hmac = crypto.createHmac(sigHashAlg, secret)
  const digest = Buffer.from(sigHashAlg + '=' + hmac.update(req.rawBody).digest('hex'), 'utf8')
  if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
    return next(`Request body digest (${digest}) did not match ${sigHeaderName} (${sig})`)
  }

  return next()
}

app.post('/webhook', verifyPostData, function (req, res) {
  res.status(200).send('Request body was signed')
})

app.use((err, req, res, next) => {
  if (err) console.error(err)
  res.status(403).send('Request body was not signed or verification failed')
})


ViteExpress.listen(app, 3000, () =>
  console.log("Server is listening on port 3000..."),
);

Leave a Comment