Skip to content

Commit 3a9a425

Browse files
authored
Merge pull request #11 from benchling/webhook-verification-app-def
Update webhook verification to use new SDK and verify() method
2 parents ac81eed + 92c35c4 commit 3a9a425

10 files changed

Lines changed: 63 additions & 13 deletions

File tree

examples/chem-sync-local-flask/README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ https://brave-wombats-poke.loca.lt
8181
2. Ensure you've been granted access to the [Benchling Developer Platform Capability](https://help.benchling.com/hc/en-us/articles/9714802977805-Access-the-Benchling-Developer-Platform).
8282
3. This example also requires a [Lab Automation](https://www.benchling.com/resources/benchling-lab-automation) license.
8383
4. [Molecule entities](https://help.benchling.com/hc/en-us/articles/9684254682893-Molecule-entity-overview) will need to be enabled on your tenant.
84+
5. [Global Apps](https://docs.benchling.com/docs/global-apps-faq) will need to be enabled on your tenant.
8485

8586
### Upload the App Manifest
8687

@@ -158,15 +159,34 @@ Open it in an editor of your choice and set the values with the plaintext client
158159
for your App. For example:
159160

160161
```
161-
CLIENT_ID=42a0cd39-0543-4dd2-af02-a866c97f0c4d
162+
CLIENT_ID=Ts7jtwPohM
162163
```
163164

165+
### Setting App Definition ID
166+
167+
The App definition ID is available from the Developer Console by selecting the App to view.
168+
169+
![image info](./docs/global-app-definition-id.png)
170+
171+
> ℹ️ **Note:** If you do NOT see this ID, please ensure [Global Apps](https://docs.benchling.com/docs/global-apps-faq) are enabled for your tenant.
172+
173+
Add it to your `.env` file with a variable name `APP_DEFINITION_ID`. The contents of your `.env` file should now look something like:
174+
175+
```
176+
CLIENT_ID=Ts7jtwPohM
177+
APP_DEFINITION_ID=appdef_Trow4zbR3o
178+
```
179+
180+
### Restarting the Container to Reflect Environment Changes
181+
164182
Restart the `benchling-app` Docker container to pick up the environment changes.
165183

166184
```bash
167185
docker-compose up -d
168186
```
169187

188+
### Security Note: Storing App Secrets in Production
189+
170190
> ⚠️ **Security Note:** In production, store the secret with a secure solution such as a secrets store (AWS Secrets Manager, as an example) or, if storing programmatically, encrypted using app-layer encryption. Avoid placing it in plaintext anywhere in code or configuration.
171191
172192
### Create App Registry Dependencies

examples/chem-sync-local-flask/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ services:
1717
# Client ID is not sensitive and is the same across all tenants, so could be hard-coded
1818
# You might choose to have two different Apps for dev vs prod, which would have different client IDs
1919
- CLIENT_ID
20+
- APP_DEFINITION_ID
2021
# Client secret for the Benchling App, stored somewhere securely in production.
2122
# Injected here for convenience. Each Client ID will have its own Client secret
2223
- CLIENT_SECRET_FILE=/run/secrets/app_client_secret
71.1 KB
Loading

examples/chem-sync-local-flask/local_app/app.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from threading import Thread
22

3-
from benchling_sdk.apps.helpers.webhook_helpers import verify_app_installation
3+
from benchling_sdk.apps.helpers.webhook_helpers import verify
44
from flask import Flask, request
55

66
from local_app.benchling_app.handler import handle_webhook
7+
from local_app.benchling_app.setup import app_definition_id
78
from local_app.lib.logger import get_logger
89

910
logger = get_logger()
@@ -20,11 +21,11 @@ def health_check() -> tuple[str, int]:
2021
@app.route("/1/webhooks/<path:target>", methods=["POST"])
2122
def receive_webhooks(target: str) -> tuple[str, int]: # noqa: ARG001
2223
# For security, don't do anything else without first verifying the webhook
23-
app_id = request.json["app"]["id"] # type: ignore[index]
24+
app_def_id = app_definition_id()
2425

2526
# Important! To verify webhooks, we need to pass the body as an unmodified string
2627
# Flask's request.data is bytes, so decode to string. Passing bytes or JSON won't work
27-
verify_app_installation(app_id, request.data.decode("utf-8"), request.headers)
28+
verify(app_def_id, request.data.decode("utf-8"), request.headers)
2829

2930
logger.debug("Received webhook message: %s", request.json)
3031
# Dispatch work and ACK webhook as quickly as possible

examples/chem-sync-local-flask/local_app/benchling_app/setup.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ def init_app_from_webhook(webhook: WebhookEnvelopeV0) -> App:
1212
return App(webhook.app.id, _benchling_from_webhook(webhook))
1313

1414

15+
@cache
16+
def app_definition_id() -> str:
17+
# App definition ID is available to "global" apps. It uniquely identifies the Benchling App
18+
# above the tenant context.
19+
#
20+
# Although it is available via the webhook, for security purposes we choose to supply it with
21+
# the App's code to avoid reusing elements of the webhook's data payload as part of its verification.
22+
# For ease of setup, we retrieve it from an environment variable.
23+
# You could choose to simply leave it in code like `return "appdef_SpzX0d5oDA"`.
24+
app_def_id = os.environ.get("APP_DEFINITION_ID")
25+
assert app_def_id is not None, "Missing APP_DEFINITION_ID from environment"
26+
return app_def_id
27+
28+
1529
def _benchling_from_webhook(webhook: WebhookEnvelopeV0) -> Benchling:
1630
return Benchling(webhook.base_url, _auth_method())
1731

examples/chem-sync-local-flask/manifest.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ manifestVersion: 1
33
info:
44
name: Sample Sync App
55
version: 0.1.0
6-
settings:
7-
lifecycleManagement: MANUAL
86
features:
97
- name: Sync Step
108
id: sync_step
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
flask~=3.0.0
1+
flask~=3.0.2
22
# Cryptography extra needed for webhook verification
3-
benchling-sdk[cryptography]==1.10.0
3+
benchling-sdk[cryptography]==1.11.0

examples/chem-sync-local-flask/ruff.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ target-version = "py311"
44
line-length = 110
55

66
select = ["ALL"]
7-
ignore = ["D100", "D101", "D103", "D104", "EM101", "EM102", "S101", "TRY003"]
7+
ignore = ["D100", "D101", "D103", "D104", "EM101", "EM102", "S101", "TRY003", "TRY301"]
88

99
[per-file-ignores]
1010
"**/tests/*" = ["ANN001", "ANN101", "D102", "PLR2004"]

examples/chem-sync-local-flask/tests/unit/local_app/benchling_app/test_setup.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44
from benchling_sdk.apps.framework import App
55

6-
from local_app.benchling_app.setup import _auth_method, init_app_from_webhook
6+
from local_app.benchling_app.setup import _auth_method, app_definition_id, init_app_from_webhook
77
from tests.helpers import load_webhook_json
88

99
_TEST_FILES_PATH = Path(__file__).parent.parent.parent.parent / "files/webhooks"
@@ -13,6 +13,7 @@ class TestBenchlingAppSetup:
1313

1414
def setup_method(self) -> None:
1515
_auth_method.cache_clear()
16+
app_definition_id.cache_clear()
1617

1718
def test_init_app_from_webhook(self, monkeypatch) -> None:
1819
webhook = load_webhook_json(_TEST_FILES_PATH / "canvas_initialize_webhook.json")
@@ -35,3 +36,14 @@ def test_init_app_from_webhook_missing_client_secret_file(self, monkeypatch) ->
3536
context.setenv("CLIENT_ID", "clientId")
3637
with pytest.raises(AssertionError, match="Missing CLIENT_SECRET_FILE from environment"):
3738
init_app_from_webhook(webhook)
39+
40+
def test_app_definition_id(self, monkeypatch) -> None:
41+
with monkeypatch.context() as context:
42+
context.setenv("APP_DEFINITION_ID", "app_def1234")
43+
result = app_definition_id()
44+
assert result == "app_def1234"
45+
46+
def test_app_definition_id_missing_app_definition_id(self, monkeypatch) -> None:
47+
with (monkeypatch.context(),
48+
pytest.raises(AssertionError, match="Missing APP_DEFINITION_ID from environment")):
49+
app_definition_id()

examples/chem-sync-local-flask/tests/unit/local_app/test_app.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ def client(app: Flask) -> FlaskClient:
2828
class TestApp:
2929

3030
@patch("local_app.app._enqueue_work")
31-
@patch("local_app.app.verify_app_installation")
32-
def test_app_receive_webhook(self, mock_verify_app_installation, mock_enqueue_work, client) -> None:
31+
@patch("local_app.app.app_definition_id")
32+
@patch("local_app.app.verify")
33+
def test_app_receive_webhook(
34+
self, mock_verify, mock_app_definition_id, mock_enqueue_work, client,
35+
) -> None:
3336
webhook = load_webhook_json(_TEST_FILES_PATH / "canvas_initialize_webhook.json")
3437
response = client.post("1/webhooks/canvas", json=webhook.to_dict())
3538
assert response.status_code == 200
36-
mock_verify_app_installation.assert_called_once()
39+
mock_verify.assert_called_once()
40+
mock_app_definition_id.assert_called_once()
3741
mock_enqueue_work.assert_called_once()

0 commit comments

Comments
 (0)