Verify Travis CI Webhook Notifications with Node.js

Travis CI allows users to configure webhook notifications which can be sent during certain build events. I recently played with this feature in order to send SMS messages via Twilio whenever a project failed to build/test. This post will discuss how to validate these webhook notifications to ensure that they are actually being sent by Travis CI.

Note: A portion of the code and this blog post were written during my 10% time at Truveris.

The Problem

The app I built was able to handle incoming requests from Travis CI, but it had no way of checking where these requests were coming from. This meant that anyone could send a request to the app pretending to be Travis CI.

The Travis CI's docs have a section on verifying webhook requests, but the instructions (shown below) were a bit unclear for someone without a lot of knowledge in encryption/encoding.

  1. Pick up the payload data from the HTTP request’s body.
  2. Obtain the Signature header value, and base64-decode it.
  3. Obtain the public key corresponding to the private key that signed the payload. This is available at the /config endpoint’s config.notifications.webhook.public_key on the relevant API server. (e.g., https://api.travis-ci.org/config)
  4. Verify the signature using the public key and SHA1 digest.

The docs even give a few examples on how to do this but none of them pertain to JavaScript. I decided to figure it out for myself, and it ended up being pretty simple.

The Solution

There are many popular Node.js web frameworks and they each handle incoming requests in different ways. This post will provide solutions with two of the most popular frameworks, Express (v4.15.3) and hapi (v16.5.0). The logic can easily be ported to work with any framework.

Both solutions will use the got library to obtain Travis's public encryption key as well as the built-in crypto library for SHA-1 verification. The Express solution will also require the body-parser middleware.

Note: The Node.js version being used at the time of this writing is v6.11.1 LTS.

Using Express

Boilerplate Express code for creating a server can be found here or you can reference this sample app. The route should look as such:

/* Imports and server config go here... */

app.post('/travis', function (req, res) {
  let travisSignature = Buffer.from(req.headers.signature, 'base64');
  let payload = req.body.payload;
  let status = false;

  got('https://api.travis-ci.org/config', {
      timeout: 10000
  })
  .then(response => {
    let travisPublicKey =
      JSON.parse(response.body).config.notifications.webhook.public_key;
    let verifier = crypto.createVerify('sha1');
    verifier.update(payload);
    status = verifier.verify(travisPublicKey, travisSignature);
  })
  .catch(error => {
    console.log('Something went wrong:\n' + error)
  })
  .then(() => {
    if (status) {
      // Handle request here now that it has been verified...
    }
    res.sendStatus(200);
  });
});

Using hapi

Boilerplate hapi code for creating a server can be found here or you can reference this sample app. The route should look as such:

/* Imports and server config go here... */

server.route({
  method: 'POST',
  path:'/travis',
  handler: function (request, reply) {
    let travisSignature = Buffer.from(request.headers.signature, 'base64');
    let payload = request.payload.payload;
    let status = false;
    
    got('https://api.travis-ci.org/config', {
      timeout: 10000
    })
    .then(response => {
      let travisPublicKey = JSON.parse(response.body).config.notifications.webhook.public_key;
      let verifier = crypto.createVerify('sha1');
      verifier.update(payload);
      status = verifier.verify(travisPublicKey, travisSignature);
    })
    .catch(error => {
      console.log('Something went wrong:\n' + error)
    })
    .then(() => {
      if (status) {
        // Handle request here now that it has been verified...
      }
      reply(200);
    });
  }
});

Ensure that the correct URL is being passed into the got request, as the .org and .com domains have different public encryption keys! These URLs are https://api.travis-ci.org/config and https://api.travis-ci.com/config, respectively.

Wrapping Up

The full source code for both the hapi and Express apps above can be found in this Github repository.

As always, thanks for reading and feel free to follow me on Twitter @brodan_ to keep up with future blog posts.

Lastly, thank you to Wayne Chang for helping me proofread and improve this post.