Validating Mandrill webhook signature on Ruby on Rails

Validating Mandrill webhook signature on Ruby on Rails

I'm writing this post partially because I couldn't find anything exactly like this and to keep a log for me, in case I have to deal with this once again.

Since Mandrill does not provide a way to authenticate their webhooks using, for example, a user/password combination, they came up with a smart way to validate their webhooks: they sign the webhook with a private key, generated during the webhook creation and you can use it to check webhook's authenticity. Mandrill says that this process is optional but, of course, it is always good to have one more layer of security in your application.

Setting up the webhook

Once you go to the Mandrill admin panel, you will provide the URL for your backend and Mandrill will then set up a webhook and generate a private key

When Mandrill starts sending events to your endpoint, the request will look like this

The signature is in the headers, accessible via X-Mandrill-Signature

Generate the signature locally

Mandrill documentation provides a comprehensive step by step on how to generate the signature and validate the webhook:

  • Create a string with the webhook's URL, exactly as you entered it in Mandrill (including any query strings, if applicable). Mandrill always signs webhook requests with the exact URL you provided when you configured the webhook. A difference as small as including or removing a trailing slash will prevent the signature from validating.
  • Sort the request's POST variables alphabetically by key.
  • Append each POST variable's key and value to the URL string, with no delimiter.
  • Hash the resulting string with HMAC-SHA1, using your webhook's authentication key to generate a binary signature.
  • Base64 encode the binary signature
  • Compare the binary signature that you generated to the signature provided in the X-Mandrill-Signature HTTP header.

There is even an example with PHP that can be easily translated to Ruby.

Creating a class to generate a signature

This is my attempt to translate the PHP code to Ruby

class MandrillNotary
  HASH_ALGORITHM = 'sha1'
  MANDRILL_WEBHOOK_KEY = 'MANDRILL_WEBHOOK_KEY'

  def self.call(url, params)
    new(url, params).call
  end

  def call
    raw_data = url + params.sort.join

    Base64.strict_encode64(
      OpenSSL::HMAC.digest(HASH_ALGORITHM, mandrill_private_key, raw_data)
    )
  end
  
  private
  
  def mandrill_private_key
    ENV[MANDRILL_WEBHOOK_KEY]
  end
  
  # private constructor and attr_reader omitted
  
end

This is class will generate a signature than can then be compared to X-Mandrill-Signature header.

It took me a couple of hours to figure out exactly what url and params should be. I tried to send different things to this class and always got the wrong signature. The secret here is to understand how Mandrill parameters are sent and what you should use.

Mandrill documentation says that they currently are sending only one parameter: mandrill_events with a bunch of events like this:

[{
	"event": "send",
	"msg": {
		"ts": 1365109999,
		"subject": "This an example webhook message",
		"email": "example.webhook@mandrillapp.com",
		"sender": "example.sender@mandrillapp.com",
		"tags": ["webhook-example"],
		"opens": [],
		"clicks": [],
		"state": "sent",
		"metadata": {
			"user_id": 111
		},
		"_id": "exampleaaaaaaaaaaaaaaaaaaaaaaaaa",
		"_version": "exampleaaaaaaaaaaaaaaa"
	},
	"_id": "exampleaaaaaaaaaaaaaaaaaaaaaaaaa",
	"ts": 1587734998
}, {
	"event": "send",
	"msg": {
		"ts": 1365109999,
		"subject": "This an example webhook message",
		"email": "example.webhook@mandrillapp.com",
		"sender": "example.sender@mandrillapp.com",
		"tags": ["webhook-example"],
		"opens": [],
		"clicks": [],
		"state": "sent",
		"metadata": {
			"user_id": 111
		},
		"_id": "exampleaaaaaaaaaaaaaaaaaaaaaaaaa1",
		"_version": "exampleaaaaaaaaaaaaaaa"
	},
	"_id": "exampleaaaaaaaaaaaaaaaaaaaaaaaaa1",
	"ts": 1587734998
}]

If you try to generate the signature using Rails's params it will fail because Rails will parse all of the parameters to JSON but Mandrill does not use the JSON to generate the signature. Mandrill parameters are something like this:

{ "mandrill_events": "long_string_of_events" }

Yes, all events are sent as one String and Mandrill use this String to generate the signature.

To get the raw parameters, we need to access the request.request_parameters, as follows:

MandrillNotary.call(request.url, request.request_parameters)

Putting it all together in the controller

# frozen_string_literal: true

class Callbacks::MandrillWebhooksController < Callbacks::BaseController
  before_action :verify_request_signature

  MANDRILL_SIGNATURE_HEAD = 'X-Mandrill-Signature'

  def create
    return head(:ok) if empty_request?

    # do something usefull with the webhook here
  end

  private

  def verify_request_signature
    return if empty_request?

    head :unprocessable_entity unless signed_request == mandrill_signature
  end

  def mandrill_signature
    request.headers[MANDRILL_SIGNATURE_HEAD]
  end

  def signed_request
    MandrillNotary.call(request.url, request.request_parameters)
  end

  def empty_request?
    events.empty?
  end

  def events
    @events ||= JSON.parse(params[:mandrill_events])
  end
end

Empty requests

You might have noticed that we are ignoring empty requests. The reason so is because Mandrill will send a webhook with empty events to check if the endpoint is up and running. We can't validate the signature yet because the private key will only be generated after a successful response from this webhook and without it, we can't do anything.

Testing

I created the following RSpec to test signature generation

# frozen_string_literal: true

describe MandrillNotary do
  describe '.call' do
    subject(:sign_request) do
      with_environment(mandrill_key) do
        described_class.call(url, params)
      end
    end

    context 'when validating the signature' do
      let(:mandrill_key) { { 'MANDRILL_WEBHOOK_KEY' => 'your_private_key' } }
      let(:expected_signature) { 'IonkvkzbSSmYpEZMp1C1BNCjIzw=' }
      let(:url) { 'https://webhook_url' }
      let(:params) do
        {
          'mandrill_events' => '[your_events_here]'
        }
      end

      it 'generates the correct hash' do
        expect(sign_request).to be_eql(expected_signature)
      end
    end
  end
end

Notice that the events inside mandrill_events are actually a String

Wrapping up

If you need to generate real data to test the validation, you can setup a webhook to a site like webhook.site and inspect the response, get real data and play with it.

Hope this helps

Icons created by Freepik - Flaticon