From 6d4699ab5e92d19864c76dc4c76d7f1d3d5cf685 Mon Sep 17 00:00:00 2001 From: "workos-sdk-automation[bot]" <255426317+workos-sdk-automation[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:34:13 +0000 Subject: [PATCH] =?UTF-8?q?fix(sdk):=20webhooks-django=20=C3=97=20codex-ba?= =?UTF-8?q?seline=20=E2=80=94=20The=20agent=20hand-rolled=20HMAC=20signatu?= =?UTF-8?q?re=20verification=20(parsing=20a=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent hand-rolled HMAC signature verification (parsing a comma-separated 't=...,v1=...' header) instead of using the official WorkOS SDK/webhook-verification helper; if the grader's test vectors are generated via the real WorkOS SDK's signing scheme, a subtly different parsing/tolerance implementation would fail invalid_signature_rejected and webhook_delivery_e2e even though it passes self-authored tests. Evidence: https://evals.workos.tools/runs/01M3BD1BXZVYHG0F10DR65TQG7/attempts/01M3BD3FYTJ2V837E7G6RAGV6C?artifact=transcript&lines=20-26#L20 Finding: https://evals.workos.tools/runs/01M3BD1BXZVYHG0F10DR65TQG7?tab=diagnosis Workflow: https://github.com/workos/evals/actions/runs/36141608655 --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index b2a7395f..85eaa070 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,36 @@ The client exposes the WorkOS API through typed namespace properties: | `client.actions` | AuthKit Actions signature verification and response signing | | `client.pkce` | PKCE code verifier/challenge helpers | +## Webhook Signature Verification + +Use `client.webhooks.verify_event()` rather than hand-rolling the HMAC check — it implements the exact scheme WorkOS signs with, and returns the deserialized event: + +```python +from django.http import HttpResponse + +from workos import WorkOSClient + +client = WorkOSClient(api_key="sk_1234", client_id="client_1234") + +# In a Django view (Flask: request.get_data() / request.headers) +def workos_webhook(request): + try: + event = client.webhooks.verify_event( + event_body=request.body, # raw bytes, never a re-serialized dict + event_signature=request.headers["WorkOS-Signature"], + secret="wh_secret_1234", # endpoint secret from the WorkOS dashboard + ) + except ValueError: + return HttpResponse(status=400) # invalid signature or stale timestamp + + print(event.event, event.id) + return HttpResponse(status=200) +``` + +`verify_header()` does the same check without deserializing, for payloads you want to handle yourself. + +The `WorkOS-Signature` header is formatted `t=, v1=` — note the `, ` separator — and `v1` is the HMAC-SHA256 of `"{timestamp}.{raw body}"` keyed with the endpoint secret. Events older (or newer) than `tolerance` seconds are rejected; it defaults to 180. Verification is local and synchronous, so it works the same on `AsyncWorkOSClient`. + ## Pagination Paginated endpoints return `SyncPage[T]` (or `AsyncPage[T]`) with built-in auto-pagination: