From e73c404427e7ba431030ae552ec45674e2924dbc Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Thu, 30 May 2013 10:52:13 +0200 Subject: [PATCH] Added support for signed webhooks See http://help.mandrill.com/entries/23704122-Authenticating-webhook-request s --- .gitignore | 1 + djrill/tests/test_mandrill_webhook.py | 34 +++++++++++++++++++++++ djrill/views.py | 39 ++++++++++++++++++++++++++- docs/history.rst | 1 + docs/usage/webhooks.rst | 11 +++++++- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c7de6dd..eb75239 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ .DS_Store ._* *.pyc diff --git a/djrill/tests/test_mandrill_webhook.py b/djrill/tests/test_mandrill_webhook.py index c4fd707..39fafee 100644 --- a/djrill/tests/test_mandrill_webhook.py +++ b/djrill/tests/test_mandrill_webhook.py @@ -1,3 +1,6 @@ +from base64 import b64encode +import hashlib +import hmac import json from django.test import TestCase @@ -39,6 +42,37 @@ class DjrillWebhookSecretMixinTests(TestCase): self.assertEqual(response.status_code, 200) +class DjrillWebhookSignatureMixinTests(TestCase): + """ + Test mixin used in optional Mandrill webhook signature support + """ + + def setUp(self): + settings.DJRILL_WEBHOOK_SECRET = 'abc123' + settings.DJRILL_WEBHOOK_SIGNATURE_KEY = "signature" + settings.DJRILL_WEBHOOK_URL = "/webhook/?secret=abc123" + + def test_incorrect_settings(self): + del settings.DJRILL_WEBHOOK_URL + with self.assertRaises(ImproperlyConfigured): + self.client.post('/webhook/?secret=abc123') + settings.DJRILL_WEBHOOK_URL = "/webhook/?secret=abc123" + + def test_unauthorized(self): + settings.DJRILL_WEBHOOK_SIGNATURE_KEY = "anothersignature" + response = self.client.post(settings.DJRILL_WEBHOOK_URL) + self.assertEqual(response.status_code, 403) + + def test_signature(self): + signature = hmac.new(key=settings.DJRILL_WEBHOOK_SIGNATURE_KEY, msg = settings.DJRILL_WEBHOOK_URL+"mandrill_events[]", digestmod=hashlib.sha1) + hash_string = b64encode(signature.digest()) + response = self.client.post('/webhook/?secret=abc123', data={"mandrill_events":"[]"}, **{"X-Mandrill-Signature" : hash_string}) + self.assertEqual(response.status_code, 200) + + def tearDown(self): + del settings.DJRILL_WEBHOOK_SIGNATURE_KEY + del settings.DJRILL_WEBHOOK_URL + class DjrillWebhookViewTests(TestCase): """ Test optional Mandrill webhook view diff --git a/djrill/views.py b/djrill/views.py index 8a420cf..87f449a 100644 --- a/djrill/views.py +++ b/djrill/views.py @@ -1,9 +1,13 @@ +from base64 import b64encode +import hashlib +import hmac import json from django import forms from django.conf import settings from django.contrib import messages from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse from django.views.generic import TemplateView, View from django.http import HttpResponse from django.utils.decorators import method_decorator @@ -100,6 +104,39 @@ class DjrillWebhookSecretMixin(object): return super(DjrillWebhookSecretMixin, self).dispatch( request, *args, **kwargs) +class DjrillWebhookSignatureMixin(object): + + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + + signature_key = getattr(settings, 'DJRILL_WEBHOOK_SIGNATURE_KEY', None) + + if signature_key and request.method == "POST": + + # Make webhook url an explicit setting to make sure that this is the exact same string + # that the user entered in Mandrill + post_string = getattr(settings, "DJRILL_WEBHOOK_URL", None) + if post_string is None: + raise ImproperlyConfigured( + "You have set DJRILL_WEBHOOK_SIGNATURE_KEY, but haven't set DJRILL_WEBHOOK_URL in the settings file.") + + signature = request.META.get("X-Mandrill-Signature", None) + if not signature: + return HttpResponse(status=403, content=u"X-Mandrill-Signature not set") + + # The querydict is a bit special, see https://docs.djangoproject.com/en/dev/ref/request-response/#querydict-objects + # Mandrill needs it to be sorted and added to the hash + post_lists = sorted(request.POST.lists()) + for value_list in post_lists: + for item in value_list[1]: + post_string += u"%s%s" % (value_list[0],item) + + hash_string = b64encode(hmac.new(key=signature_key, msg=post_string, digestmod=hashlib.sha1).digest()) + if signature != hash_string: + return HttpResponse(status=403, content=u"Signature doesn't match") + + return super(DjrillWebhookSignatureMixin, self).dispatch( + request, *args, **kwargs) class DjrillIndexView(DjrillApiMixin, TemplateView): template_name = "djrill/status.html" @@ -161,7 +198,7 @@ class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin, return self.render_to_response(context) -class DjrillWebhookView(DjrillWebhookSecretMixin, View): +class DjrillWebhookView(DjrillWebhookSecretMixin,DjrillWebhookSignatureMixin, View): def head(self, request, *args, **kwargs): return HttpResponse() diff --git a/docs/history.rst b/docs/history.rst index b3ce8e7..b04785a 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -3,6 +3,7 @@ Release Notes Version 0.6 (development): +* Support for signed webhooks Version 0.5: diff --git a/docs/usage/webhooks.rst b/docs/usage/webhooks.rst index 8874cf5..0cde41d 100644 --- a/docs/usage/webhooks.rst +++ b/docs/usage/webhooks.rst @@ -29,10 +29,13 @@ Your code can connect to this signal for further processing. app and Mandrill. Djrill will verify calls to your webhook, and will reject calls without the correct key. + * You can, optionally include the two settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` + and :setting:`DJRILL_WEBHOOK_URL` to enforce webhook signature checking + .. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks .. _securing webhooks: http://apidocs.mailchimp.com/webhooks/#securing-webhooks - +.. _webhook signatures: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests .. _webhooks-config: @@ -97,6 +100,12 @@ the url config in step 2. And if you'd like to change the *name* of the "secret" query string parameter, you can set :setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`. +For extra security, Mandrill provides a signature in the request header +X-Mandrill-Signature. If you want to verify this signature, you need to provide +the settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` with the webhook-specific +signature key that can be found in the Mandrill admin panel and +:setting:`DJRILL_WEBHOOK_URL` where you should enter the exact URL, including +that you entered in Mandrill when creating the webhook. .. _webhooks control panel: https://mandrillapp.com/settings/webhooks .. _inbound settings: https://mandrillapp.com/inbound