From 180768a4143fdf5de093b1500c370166057e67d1 Mon Sep 17 00:00:00 2001 From: kwong Date: Wed, 12 Feb 2025 10:24:56 -0500 Subject: [PATCH 1/3] add signature verification --- .fernignore | 1 + src/webflow/signature.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/webflow/signature.py diff --git a/.fernignore b/.fernignore index b6293ae..07e7613 100644 --- a/.fernignore +++ b/.fernignore @@ -4,3 +4,4 @@ README.md assets/ src/webflow/oauth.py +src/webflow/signature.py diff --git a/src/webflow/signature.py b/src/webflow/signature.py new file mode 100644 index 0000000..0e64bc1 --- /dev/null +++ b/src/webflow/signature.py @@ -0,0 +1,21 @@ +# If we can I'd like to move this to the webhooks client wrapper, +# but I can't find the Fern generation.yml to extend the client +# according to documentation included here: +# https://buildwithfern.com/learn/sdks/capabilities/custom-code +import hmac +import hashlib +from collections.abc import Mapping + +def verify(headers: Mapping, body:str , secret: str): + # Normalize header format to account for different server implementations + normalized_headers = {k.lower(): v for k, v in headers.items()} + + message = f"{normalized_headers.get('x-webflow-timestamp', '')}:{body}".encode('utf-8') + + generated_signature = hmac.new( + key=secret.encode('utf-8'), + msg=message, + digestmod=hashlib.sha256 + ).hexdigest() + + return normalized_headers.get("x-webflow-signature", "") == generated_signature \ No newline at end of file From 7f4030a52aaaabacb60d5a51aecfecc4b8916442 Mon Sep 17 00:00:00 2001 From: kwong Date: Wed, 12 Feb 2025 11:52:30 -0500 Subject: [PATCH 2/3] add comment --- src/webflow/signature.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webflow/signature.py b/src/webflow/signature.py index 0e64bc1..d6adb37 100644 --- a/src/webflow/signature.py +++ b/src/webflow/signature.py @@ -7,7 +7,8 @@ from collections.abc import Mapping def verify(headers: Mapping, body:str , secret: str): - # Normalize header format to account for different server implementations + # Normalize header format to account for different python server implementations + # that may or may not normalize headers already normalized_headers = {k.lower(): v for k, v in headers.items()} message = f"{normalized_headers.get('x-webflow-timestamp', '')}:{body}".encode('utf-8') From 3b677ec6be7dafbc491faecad27a3f5143e1ec0e Mon Sep 17 00:00:00 2001 From: kwong Date: Wed, 12 Feb 2025 12:06:40 -0500 Subject: [PATCH 3/3] add docs --- src/webflow/signature.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/webflow/signature.py b/src/webflow/signature.py index d6adb37..9d67dd3 100644 --- a/src/webflow/signature.py +++ b/src/webflow/signature.py @@ -7,6 +7,47 @@ from collections.abc import Mapping def verify(headers: Mapping, body:str , secret: str): + """ + Verify that the signature on the webhook message is from Webflow + + Documentation: + https://developers.webflow.com/data/docs/working-with-webhooks#validating-request-signatures + + Parameters: + - headers : Mapping. The request headers in a Mapping-like object + + - body : str. The request body as a UTF-8 encoded string + + - secret : str. The secret key generated when creating the webhook or the OAuth client secret + --- + ``` + from fastapi import FastAPI, Request + # ... + + @app.post('/webhookEndpoint') + async def webhook_endpoint_handler( + request: Request, + ): + # Read the request body as a utf-8 encoded string + body = (await request.body()).decode("utf-8") + + # Extract the headers as Mapping-like object + headers = request.headers + + secret = get_secret() + + verified = verify( + headers=request.headers, + body=(await request.body()).decode("utf-8"), + secret=new_webhook.secretKey) + + if verified: + # ...process the request normally + else: + # ...handle unathenticated request + ``` + """ + # Normalize header format to account for different python server implementations # that may or may not normalize headers already normalized_headers = {k.lower(): v for k, v in headers.items()}