Was this page helpful?

Request Verification

Table of contents

How does webhook request verification work?

When you've configured a WebhookSigningSecret in the webhook settings of your space, a signature will be generated for all webhook requests coming from events in that space. When a consuming service receives a request, it can compute a signature in the same way. If the secret is the same, the signatures will match too, thus verifying that the sender knows the secret. For this reason, it is also important to not share the secret with any third party.

This signature and some additional metadata is then added to the headers of the request before it is sent. The headers look like this:

Header Name Value
x-contentful-signature The computed signature of an individual request.
x-contentful-signed-headers A comma-separated list of headers included in the computed signature.
x-contentful-timestamp Timestamp of when the request was signed. Can be used to ensure a TTL and avoid processing stale or replayed events.

Consuming services can use these headers to verify the authenticity and integrity of incoming webhooks. See verifyRequest in node-apps-toolkit for more information.

Get started with Webhook Request Verification

Establishing a signing secret

A signing secret can be generated in the settings tab of the webhook settings page within a space. Click on Enable request verification in this tab, and a signing secret will be generated.

  1. Go to the space that you want to enable request verification on webhooks.
  2. Click Settings -> Webhooks. Webhook space settings
  3. Once on the Webhooks space settings page, click on the "Settings" tab.
  4. Click the Enable request verification button to create the signing secret. Generated signing secret
  5. Copy the generated siging secret. You will not be able to copy it again once you navigate away from this view. Enabling request verification

You can also manage secrets directly using the Content Management API (CMA). Visit the API documentation for details on how to manage the signing secret by using the API.

Once request verification has been enabled, all of the webhooks in the space will include signed request headers which can be used to verify their authenticity and integrity.

Verifying requests on the backend

Now that requests from your app contain the additional signature headers, your backend can start to enforce signed requests. The following is an example server in Node.js using the Express framework:

const express = require('express');
const { verifyRequest } = require('@contentful/node-apps-toolkit');
const app = express();
app.use(
  express.json({
    type: ['application/json', 'application/vnd.contentful.management.v1+json'],
  })
);

// This is the secret key that was generated by you or Contentful.
// Note: it is not best practice to hardcode secret keys into code.
// We are doing it here for demonstration purposes only. Preferably
// it should come from an environment variable or a file.
const secret = 'your-64-char-secret';

// This can be the endpoint configured to be called for webhook events
app.post('/webhook-event-handler', (req, res) => {
  const canonicalRequest = {
    path: req.path,
    headers: req.headers,
    method: req.method,
    body: JSON.stringify(req.body),
  };
  try {
    const isValid = verifyRequest(secret, canonicalRequest);
    if (!isValid) {
      res.status(403).send('Unauthorized');
    }
  } catch (e) {
    console.error(e);
    res.status(403).send('Unauthorized');
  }

  // ... rest of handler code
});

const port = 9000;
app.listen(port, () =>
  console.log(`App listening at http://localhost:${port}`)
);

This example uses node-apps-toolkit, which contains tools to create app backends in Node.js. The verifyRequest function will try to sign the contents of the incoming request and then compare them to the meta-information contained in the signing headers. The function will throw an error if the incoming request is shaped incorrectly or is older than the specified time-to-live (TTL). You can pass the TTL in seconds as an additional argument, or disable it by passing 0. If nothing is passed, the TTL will be 30 seconds. The documentation for node-apps-toolkit contains more detailed information on the signing and verification process.

If you are developing in a different programming language, you will likely have to implement the verifyRequest function yourself. There is a pseudocode implementation of the verification algorithm at the bottom of this page which can be used as a reference for your implementation.

Key rotation

Changing your secret while you are already enforcing secure requests requires some additional steps. This is because changing the secret on either side will cause verification to fail until the other side has been updated too. We can get around this by verifying against multiple secrets simultaneously in the backend. The process looks like this:

  1. Generate a new secret
  2. Verify requests against both the old and the new secret in your backend
  3. Submit the new secret to Contentful using the web app or the API, causing further requests to be signed with the new key
  4. Remove support for the old key from your backend

The following sections will go into detail on how to generate a secret and how to add support for multiple secrets in an backend.

Creating a secret

There are two constraints on the secret:

  1. It must be exactly 64 characters long.
  2. It must match this regular expression: /^[0-9a-zA-Z+/=_-]+$/. This also means secrets using the hex or base64 character set are allowed.

To make full use of the security provided by secure requests, we recommend using cyptographic random number generators with high entropy to generate a secret. Here are some examples for how to do this using different languages and tools. All examples can be run on the command line:

# Node.js >6.0
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Ruby
ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'
# Python >3.6
python3 -c "from secrets import token_hex;print(token_hex(32))"
# OpenSSL
openssl rand -hex 32

Verify requests against multiple secrets

To properly test against multiple secrets, an attempt to verify the incoming request has to be made separately for each secret, as the request should only be rejected if none of the secrets can verify the request. The following is an example in Node.js using the Express framework:

const express = require('express');
const { verifyRequest } = require('@contentful/node-apps-toolkit');
const app = express();
app.use(
  express.json({
    type: ['application/json', 'application/vnd.contentful.management.v1+json'],
  })
);

// List multiple secrets, both the new and soon-to-be-disabled ones.
// Note: it is not best practice to hardcode secret keys into code.
// We are doing it here for demonstration purposes only. Preferably
// they should come from an environment variable or a file.
const secrets = ['old-64-char-secret', 'new-64-char-secret'];

app.post('/webhook-event-handler', (req, res) => {
  const canonicalRequest = {
    path: req.path,
    headers: req.headers,
    method: req.method,
    body: JSON.stringify(req.body),
  };
  try {
    const isValid = secrets.some((secret) =>
      verifyRequest(secret, canonicalRequest)
    );
    if (!isValid) {
      res.status(403).send('Unauthorized');
    }
  } catch (e) {
    console.error(e);
    res.status(403).send('Unauthorized');
  }

  // ... rest of handler code
});

const port = 9000;
app.listen(port, () =>
  console.log(`App listening at http://localhost:${port}`)
);

Verifying a signature

This is a pseudocode example of how signed requests can be verified. If you need to verify a request in another programming language you can use this as the basis for your implementation. In addition a working JavaScript implementation can be found in the node-apps-toolkit.

// The signing function takes a signing key and the request to be signed
function verifyRequest(signingKey, request) {
  // The signature, which we will be verifying the request against
  signature = getHeader('x-contentful-signature', request.headers)

  // The time when the request was signed, which can be used for a TTL check
  timestamp = getHeader('x-contentful-timestamp', request.headers)
  oneMinute = 60 * 1000
  if (Number(timestamp) + oneMinute < currentUnixTimeInMilliseconds) {
    return false
  }

  // one of GET, PUT, POST, DELETE etc.
  requestMethod = request.method

  // The request path excluding protocol, hostname, and port.
  // Any query strings should be url encoded, and the entire path
  // should be utf-8 encoded.
  [basePath, query] = request.path.split('?')
  query = urlEncode(query)
  requestPath = utf8Encode(query ? basePath + '?' + query : basePath)

  // The headers which have been signed
  signedHeaders = getHeader('x-contentful-signed-headers', request.headers)

  // The headers should be converted to lower case, and joined in a semicolon
  // separated list. The list should maintain the ordering from
  // 'x-contentful-signed-headers'
  requestHeaders = []
  for (headerName in signedHeaders.split(',')) {
    headerValue = getHeader(headerName, request.headers)

    requestHeaders.push(headerName.toLowerCase() + ':' + headerValue)
  }
  requestHeaders = requestHeaders.join(';')

  // The request body, exactly as it is in the HTTP body
  requestBody = request.body


  // The method, path, headers, and request body joined in a newline separated string
  canonicalRequestRepresentation = [
    requestMethod,
    requestPath,
    requestHeaders,
    requestBody
  ].join('\n')


  // HMAC_SHA256 hash digested as an hexadecimal string
  signature = createSignature(canonicalRequestRepresentation, signingKey)

  // We can now test for equality of the requestSignature, and the signature we
  // have generated and return the result
  return signature === requestSignature
}

Feedback

Our Developer Experience team highly welcomes feedback for all our features. To share your feedback, you can fill in this form.