Skip to content

Commit

Permalink
Add optional signature for webhooks (#1115)
Browse files Browse the repository at this point in the history
This update:

* adds an optional webhook_secret field an OAuth2 project can provide
* if provided, this is used to create an hmac-sha1 hexdigest to verify
a request made to a webhook
* this is included as a custom header "X-OpenHumans-Webhooks-Signature"
when calling a webhook
* documentation is added for the deauthorization webhook and webhook secret
* the JSON payload for the deauthorization webhook has been corrected
(it was accidentally doing a second JSON encoding)

The webhook wasn't actually documented before now, but the last item would break something already using the webhook. So as a fallback, the server tries again with the old double-encoded version if a non-200 response is received. An update supporting the new behavior is also staged for the django-open-humans package.
  • Loading branch information
madprime authored Jul 16, 2020
1 parent 640d183 commit a2e3bf3
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 6 deletions.
1 change: 1 addition & 0 deletions private_sharing/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class Meta: # noqa: D101
"enrollment_url",
"terms_url",
"redirect_url",
"webhook_secret",
"deauth_webhook",
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 2.2.10 on 2020-07-01 21:05

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("private_sharing", "0026_auto_20191202_2105")]

operations = [
migrations.AddField(
model_name="oauth2datarequestproject",
name="webhook_secret",
field=models.CharField(
blank=True,
help_text="If entered, this string will be used to provide a hash verifying Open Humans as the sender.",
max_length=64,
validators=[
django.core.validators.RegexValidator(regex="[\x00-\x7f]*"),
django.core.validators.MinLengthValidator(16),
],
),
)
]
37 changes: 31 additions & 6 deletions private_sharing/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hmac
import re

from distutils.util import strtobool
Expand All @@ -13,7 +14,7 @@

from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import ArrayField
from django.core.validators import URLValidator
from django.core.validators import MinLengthValidator, RegexValidator, URLValidator
from django.core.exceptions import ValidationError
from django.db import models, router
from django.db.models import F
Expand Down Expand Up @@ -391,7 +392,12 @@ class Meta: # noqa: D101
),
verbose_name="Redirect URL",
)

webhook_secret = models.CharField(
max_length=64,
validators=[RegexValidator(regex="[\x00-\x7F]*"), MinLengthValidator(16)],
blank=True,
help_text="""If entered, this string will be used to provide a hash verifying Open Humans as the sender.""",
)
deauth_webhook = models.URLField(
blank=True,
default="",
Expand Down Expand Up @@ -549,16 +555,35 @@ def deauth_webhook(self):
"""
erasure_requested = bool(self.erasure_requested)

slug = {
payload = {
"project_member_id": self.project_member_id,
"erasure_requested": erasure_requested,
"timestamp": arrow.get(self.erasure_requested).isoformat(),
}

url = self.project.oauth2datarequestproject.deauth_webhook
json_p = json.dumps(slug)
json_payload = json.dumps(payload).encode("utf-8")

webhook_secret = self.project.oauth2datarequestproject.webhook_secret.encode(
"utf-8"
)
headers = {"Content-Type": "application/json"}
if webhook_secret:
signature = (
"sha1="
+ hmac.new(
key=webhook_secret, msg=json_payload, digestmod="sha1"
).hexdigest()
)
headers["X-OpenHumans-Webhooks-Signature"] = signature

request_post = requests.post(url, data=json_payload, headers=headers)

# 202007 legacy support: previously, JSON was accidentally double-encoded.
if not (200 <= request_post.status_code <= 299):
request_post = requests.post(url, json=json_payload.decode("utf-8"))

request_p = requests.post(url, json=json_p)
return request_p.status_code
return request_post.status_code

def leave_project(
self, remove_datafiles=False, done_by=None, erasure_requested=False
Expand Down
48 changes: 48 additions & 0 deletions private_sharing/templates/direct-sharing/oauth2-setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ <h2>OAuth2 setup</h2>
<li>
<a href="#setup-oauth2-authorization">OAuth2 authorization setup</a>
</li>
<li>
<a href="#setup-deauth-webhooks">Optional deauthorization webhooks</a>
</li>
</ul>

{% include 'direct-sharing/partials/setup.html' with oauth2_project=True %}
Expand Down Expand Up @@ -183,4 +186,49 @@ <h3 id="setup-oauth2-authorization">OAuth2 authorization setup</h3>
</p>
</li>
</ol>

<h3 id="setup-deauth-webhooks">Webhooks</h3>

<h4>Deauthorization Webhook URL (optional)</h4>
<p>
A webhook can optionally be provided to automate handling when members leaving
(deauthorize) an activity (for example, a request to erase personal data,
to comply with GDPR). If provided, when a member leaves an activity a POST
will be sent to that URL with JSON formatted data containing the following
fields:
</p>
<ul>
<li>
<code>project_member_id</code>: (string) identifying the project member performing deauthorization
</li>
<li>
<code>erasure_requested</code>: (true/false) whether the deauthorizing member is requesting the activity delete their personal data
</li>
<li>
<code>timestamp</code>: (string) ISO 8601 format timestamp of the deauthorization
</li>
</ul>

<h4>Webhook secret (optional)</h4>
<p>
A webhook secret can optionally be provided, to be used to verify that
incoming requests sent to the webhook are coming from Open Humans.
If provided, POSTs to the webhook will use the secret to provide an hmac-sha1
a verification signature via a custom <code>X-OpenHumans-Webhooks-Signature</code>
header. The value of this header is the string <code>sha1=</code> followed by
the hmac-sha1 hexdigest of the payload.
</p>
<p>
For example, the secret: <code>abcdefghijklmnop</code><br>
would be used in combination with a payload:<br><code>{"project_member_id": "12345678", "erasure_requested": true, "timestamp": "2020-06-30T20:00:00.000000+00:00"}</code><br>
to provide the following signature in the header: <code>sha1=ebda5c0a38593a4350ad8401b7d8c8f1cd08ec67</code>
</p>
<pre>
# example Python code for generating this string
import hmac

key = 'abcdefghijklmnop'.encode('utf-8')
payload = '{"project_member_id": "12345678", "erasure_requested": true, "timestamp": "2020-06-30T20:00:00.000000+00:00"}'.encode('utf-8')
sig = 'sha1=' + hmac.hex
</pre>
{% endblock %}

0 comments on commit a2e3bf3

Please sign in to comment.