fix(webhooks): authenticate deliveries via WEBHOOK_SECRET; review nits

Adds optional shared-secret authentication for /webhooks/nextcloud,
addressing the security follow-up flagged in #747.

Behavior:
- WEBHOOK_SECRET set: registrations pass authMethod="header" with
  authData={"Authorization": "Bearer <secret>"} (encrypted at-rest in
  Nextcloud's DB and forwarded on every delivery). The receiver
  validates the same header with hmac.compare_digest before parsing
  any payload; missing/invalid → 401.
- WEBHOOK_SECRET unset: registrations stay on authMethod="none" and
  the receiver accepts unauthenticated POSTs (logging a one-time
  startup warning). Backward compatible — operators can roll out at
  their own pace.

Implementation notes:
- WebhooksClient.create_webhook gains an `auth_data` parameter mapped
  to NC's `authData` body field; this is distinct from the existing
  `headers` parameter (`headers` is plaintext static request headers,
  `authData` is encrypted at-rest in NC and only emitted when
  authMethod="header"). The previous `auth_method="bearer"` mention in
  the docstring was incorrect — NC supports only "none" and "header".
- A small `webhook_auth_pair()` helper in auth/webhook_routes.py
  centralises the secret→(auth_method, auth_data) resolution so the
  preset flow and the Astrolabe-facing /api/v1/webhooks endpoint stay
  in sync.

Also addresses the smaller review points from #747:
- f-string → lazy %s formatting in webhook_receiver.py and
  webhook_routes.py.
- Move `int(time)` inside webhook_parser's try/except so a malformed
  `time` field returns None instead of raising ValueError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-30 03:50:28 +02:00
co-authored by Claude Opus 4.7
parent 2e2a098bee
commit 224428fca5
10 changed files with 306 additions and 12 deletions
+12 -2
View File
@@ -41,6 +41,7 @@ class WebhooksClient(BaseNextcloudClient):
http_method: str = "POST",
auth_method: str = "none",
headers: Optional[Dict[str, str]] = None,
auth_data: Optional[Dict[str, str]] = None,
event_filter: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Register a new webhook for the specified event.
@@ -49,8 +50,14 @@ class WebhooksClient(BaseNextcloudClient):
event: Fully qualified event class name (e.g., "OCP\\Files\\Events\\Node\\NodeCreatedEvent")
uri: Webhook endpoint URL to receive event notifications
http_method: HTTP method for webhook delivery (default: "POST")
auth_method: Authentication method ("none", "bearer", etc.)
headers: Custom headers to include in webhook requests (e.g., Authorization header)
auth_method: Authentication method. Nextcloud's webhook_listeners
app accepts only ``"none"`` or ``"header"``.
headers: Optional static request headers attached to every
delivery (stored in clear text on the NC side).
auth_data: When ``auth_method="header"``, a dict of headers
holding the auth credentials. Stored encrypted at-rest in
Nextcloud's database and merged into the delivery request
at send time. Required when ``auth_method="header"``.
event_filter: JSON object specifying event filters (e.g., {"user.uid": "bob"})
Returns:
@@ -66,6 +73,9 @@ class WebhooksClient(BaseNextcloudClient):
if headers:
data["headers"] = headers
if auth_data:
data["authData"] = auth_data
if event_filter:
data["eventFilter"] = event_filter