Skip to content
Permalink
Browse files

Merge pull request #6 from muzooka/webhooks-python-example

Webhooks HMAC Update
  • Loading branch information...
MarcLindenbach committed Jun 10, 2019
2 parents 1a6244d + d554bee commit 8aa85ff9bf71673587fee5bcd0dc13460bc629b2
@@ -0,0 +1,2 @@
venv/
__pycache__/
@@ -0,0 +1,47 @@
Python Webhooks Example Server
==============================

In this example Flask and [ngrok](https://ngrok.com/) are used to create a simple server that can
create and listen to Webhooks from Muzooka's API. In this example server HMAC authentication has
been implemented so you can verify that the webhook came from Muzooka.

This example was written in Python 3, it may work with Python 2, but it has not been tested.


## Setting Up
- Create an account for [ngrok](https://ngrok.com/) and install the binary on your system
- Either clone this repository, or copy `server.py` and `requirements.txt` to your local machine
- Install the requirements using `pip install -r requirements.txt`, we suggest you use
[Virtualenv](https://virtualenv.pypa.io/) to isolate your environment
- In one terminal, start ngrok: `./ngrok http 5000`
- Note the `Forwarding` http address (it will look something like `https://xxx.ngrok.io`
- In another terminal, start the flask server:
`FLASK_APP=server.py MUZOOKA_API_KEY=xxx NGROK_URL=https://xxx.ngrok.io flask run`


## Testing
- Create a new webhook by POSTing to `localhost:5000/webhooks` with a JSON body similar to the
following, replacing `{{muzookId}}` with the page you want to follow (should be a page that you can
make changes to):
```json
{
"muzookaId": "{{muzookaId}}",
}
```
- Example cURL statement:
```bash
curl -X POST \
http://localhost:5000/webhooks \
-H 'Content-Type: application/json' \
-H 'content-length: 34' \
-d '{"muzookaId": "foofighters"}'
```
- Make a change to the page in Muzooka and wait for the change to propagate (15 minutes)
- You should see in your console a statement like so:
```
Webhook received for artist: Some Artist
Webhook signature: sha1=d4f8a60ac0c30dcf76a4052ec61e4b1aa7637603
Calculated signature: sha1=d4f8a60ac0c30dcf76a4052ec61e4b1aa7637603
Signatures match, content came from Muzooka
```
- Upon closing the server, any webhooks you created will be automatically deleted
@@ -0,0 +1,11 @@
certifi==2019.3.9
chardet==3.0.4
Click==7.0
Flask==1.0.3
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
requests==2.22.0
urllib3==1.25.3
Werkzeug==0.15.4
@@ -0,0 +1,96 @@
import os
import atexit
import json
import requests
import hashlib
import hmac
from flask import Flask, request

MUZOOKA_API_KEY = os.environ.get('MUZOOKA_API_KEY')
MUZOOKA_API_URL = 'https://devapi.muzooka.com/v2'
NGROK_URL = os.environ.get('NGROK_URL')
MUZOOKA_AUTH_HEADERS = {
'x-api-key': MUZOOKA_API_KEY,
}

app = Flask(__name__)
my_webhooks = []

if not MUZOOKA_API_KEY:
print('MUZOOKA_API_KEY is missing')
if not NGROK_URL:
print('NGROK_URL is missing')


# Creates an HMAC SHA1 signature using a key and payload
def make_signature(key, payload):
key = bytes(key, 'UTF-8')
digester = hmac.new(key, payload, hashlib.sha1)
signature = digester.hexdigest()
return 'sha1={}'.format(signature)


# On application exit all existing webhooks are deleted
def on_app_exit():
for webhook in my_webhooks:
print('deleting webhook: {}'.format(webhook['id']))
requests.delete(
'{}/webhooks/{}'.format(MUZOOKA_API_URL, webhook['id']),
headers=MUZOOKA_AUTH_HEADERS
)


atexit.register(on_app_exit)

# Use this route to create a new webhook, expects a muzookaId to exist in the
# request body, and for the body to be in JSON format. Returns the webhook id
# and the Muzooka id of the created webhook.
@app.route('/webhooks', methods=['POST'])
def create_webhook():
content = request.json
muzooka_id = content['muzookaId']

webhook_payload = {
'type': 'page',
'filter': muzooka_id,
'url': '{}/webhooks-listen'.format(NGROK_URL),
}
webhook_response = requests.post(
'{}/webhooks'.format(MUZOOKA_API_URL),
json=webhook_payload,
headers=MUZOOKA_AUTH_HEADERS,
)

webhook = {
'id': webhook_response.json()['id'],
'muzooka_id': muzooka_id,
}

my_webhooks.append(webhook)
return json.dumps(webhook)


# This route listens for updates from Muzooka, when an update is received the
# signature of the request body is calculated and compared to the signature
# provided by Muzooka
@app.route('/webhooks-listen', methods=['POST'])
def webhooks_listen():
raw_content = request.get_data()
artist = request.json
signature = request.headers.get('X-Signature')
calculated_signature = make_signature(MUZOOKA_API_KEY, raw_content)

name = artist['name'] if (artist and 'name' in artist) else 'undefined'
print('Webhook received for artist: {}'.format(name))
print('Webhook signature: {}'.format(signature))
print('Calculated signature: {}'.format(calculated_signature))

if signature == calculated_signature:
print('Signatures match, content came from Muzooka')
# This is where you would update your local store for the given artist
else:
print('Signatures do not match, content did not come from Muzooka')
# Do not update you local store with the data provided, this request
# may not have come from Muzooka

return 'ok'
@@ -75,6 +75,20 @@ Response structure is in JSON.
Success result has HTTP status 200 and body `{"message":"ok"}`.
Error result has HTTP status other than 200 and body similar to `{"message":"error description goes here"}`.

## Verifying a webhook

Whenever you receive a webhook you should verify that the message actually came from Muzooka. Every webhook that Muzooka POSTs includes a `X-Signature` header with the SHA1 signature of the request body. The signature is calculated using HMAC where the key is your API key. The signature is prepended with `sha1=`.

Here is an example of how you would calculate the HMAC signature in node.
```javascript
const crypto = require('crypto');
const calculateSignature(key, payload) {
const signature = crypto.createHmac('sha1', key).update(payload).digest('hex');
return `sha1=${signature}`;
};
```

## Good practices

- Respond as quickly as possible. Muzooka server will wait for HTTP status 200 from your server for 3 seconds before declaring webhook call a failed attempt, so it makes sense to design your app in a way where it would respond to Muzooka call right away before handling any business logic (i.e. updating caches, etc).

0 comments on commit 8aa85ff

Please sign in to comment.
You can’t perform that action at this time.