Added support for signed webhooks

See
http://help.mandrill.com/entries/23704122-Authenticating-webhook-request
s
This commit is contained in:
Jens Alm
2013-05-30 10:52:13 +02:00
parent 32c8a1643b
commit e73c404427
5 changed files with 84 additions and 2 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea/
.DS_Store
._*
*.pyc

View File

@@ -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

View File

@@ -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()

View File

@@ -3,6 +3,7 @@ Release Notes
Version 0.6 (development):
* Support for signed webhooks
Version 0.5:

View File

@@ -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