Skip to content

Add incoming webhook parsing and verification#6

Merged
loevgaard merged 1 commit into
2.xfrom
add-webhook-parsing
Jun 23, 2026
Merged

Add incoming webhook parsing and verification#6
loevgaard merged 1 commit into
2.xfrom
add-webhook-parsing

Conversation

@loevgaard

@loevgaard loevgaard commented Jun 23, 2026

Copy link
Copy Markdown
Member

What

Adds first-class support for receiving Shipmondo webhooks. Until now the SDK could only manage webhook subscriptions ($client->webhooks()->create()/delete()/getPage()); there was nothing to help a consumer verify and parse the webhooks Shipmondo POSTs to their server.

A delivery is {"data": "<JWT>"} — the JWT HS256-signed with the webhook key — plus five SMD-* metadata headers. Verifying that signature is the only proof a request genuinely came from Shipmondo.

New surface (Setono\Shipmondo\Webhook\)

use Setono\Shipmondo\Webhook\WebhookParserInterface;

$event = $parser->parse($serverRequest, $webhookKey); // PSR-7
// or, framework-agnostic:
$event = $parser->parsePayload($rawBody, $headers, $webhookKey);

$event->action;       // 'create', 'cancel', ... (SMD-Action)
$event->resourceType; // 'Shipments', 'Orders', ... (SMD-Resource-Type)
$event->resourceId;   // ?int (SMD-Resource-Id)
$event->data;         // array<array-key, mixed> — the resource, snake_case as in the API docs
  • WebhookParser implements WebhookParserInterface, so consumers can type-hint the interface and inject the parser (or a mock) via DI. (The SDK has no endpoint interfaces by design, but the parser isn't an endpoint — it's a standalone, injectable service.)
  • WebhookEvent is a plain VO carrying the five SMD-* headers plus the verified JWT payload (webhookName, url, data). No $raw property — the envelope's three claims are all already typed properties, so a Resource-style $raw would be redundant; data is itself the untyped escape hatch for the resource.
  • The parser is standalone (decoupled from Client — it uses the per-webhook key, not API credentials), stateless, and takes the key per call so a multi-webhook server can pick the key by SMD-Webhook-Id.
  • No typed mapping of data (payload shapes can drift from GET responses).

Security

  • Verification pins HS256, so alg:none and algorithm-confusion tokens are rejected.
  • New exception branch off the ShipmondoException marker (no HTTP response, so it does not extend ResponseAwareException): WebhookExceptionWebhookVerificationException (bad signature / wrong key → respond 401/403) and MalformedWebhookException (bad JSON / missing data / non-HS256 token → 400).

Dependency: firebase/php-jwt: ^7.0

Pinned to v7, not v6: v6 carries advisory CVE-2025-45769 ("short HMAC key allowed"). The trade-off is that v7 enforces a minimum HMAC key length on verification (HS256 ⇒ ≥32 bytes / 256 bits, per RFC 7518 §3.2). So the SDK now enforces that same floor:

  • WebhookRequest::$key is validated at construction (the one validated Payload field — a deliberate, documented exception to the "no construction-time Assert" convention).
  • WebhookParser re-checks at verify time, so a webhook created outside the SDK with a short key fails with a clear "key must be ≥32 bytes" error rather than a confusing decode failure.

⚠️ Behavior note: webhook keys must now be ≥32 bytes. The existing WebhooksEndpointTest fixture (key: 'secret') was updated accordingly.

Docs

  • README gains a "Receiving webhooks" section (incl. the key-length requirement and the interface for DI).
  • CLAUDE.md updated: dependency rationale, the WebhookRequest.key validation exception, and the webhook-parsing architecture.

Verification

  • PHPStan max: clean
  • ECS: clean
  • PHPUnit: 75 tests / 193 assertions (3 live-API tests skipped)
  • composer validate --strict + normalize: clean; --prefer-lowest resolves firebase/php-jwt v7.0.0 on PHP 8.1
  • composer audit: no advisories

Comment thread src/Webhook/WebhookParser.php
Adds a standalone WebhookParser that verifies and parses the webhooks
Shipmondo POSTs to a consumer's server. Previously the SDK could only
manage webhook subscriptions via the API.

A delivery is `{"data": "<JWT>"}` (HS256-signed with the webhook key) plus
five SMD-* metadata headers. WebhookParser verifies the signature — the
only proof the request came from Shipmondo — and returns a typed
WebhookEvent exposing the header metadata and the decoded resource data.

- WebhookParser::parse(ServerRequestInterface, key) for PSR-7, and
  parsePayload(body, headers, key) for non-PSR-7 frameworks.
- Verification pins HS256, rejecting alg:none / algorithm-confusion tokens.
- New exception branch off ShipmondoException: WebhookException ->
  WebhookVerificationException (bad signature -> 401/403) and
  MalformedWebhookException (bad JSON / non-HS256 token -> 400).
- Uses firebase/php-jwt ^7.0 (v7, not v6, to avoid CVE-2025-45769). v7
  enforces a >=32-byte HS256 key, so the same floor is enforced when
  creating webhooks (WebhookRequest::$key) and when verifying them.
- README "Receiving webhooks" section and CLAUDE.md updated.
@loevgaard loevgaard force-pushed the add-webhook-parsing branch from 5964fdf to 71f69fd Compare June 23, 2026 10:03
@loevgaard loevgaard merged commit 7b943c9 into 2.x Jun 23, 2026
21 checks passed
@loevgaard loevgaard deleted the add-webhook-parsing branch June 23, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant