Skip to content

feat: sign outbound webhooks with hmac-sha256#197

Merged
dan2k3k4 merged 1 commit into
devfrom
advisor/007-sign-outbound-webhooks-hmac
Jul 2, 2026
Merged

feat: sign outbound webhooks with hmac-sha256#197
dan2k3k4 merged 1 commit into
devfrom
advisor/007-sign-outbound-webhooks-hmac

Conversation

@dan2k3k4

@dan2k3k4 dan2k3k4 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Signs outbound webhooks with an HMAC-SHA256 signature so consumers can verify authenticity. Builds on the webhook test branch (advisor/006); merge that first.

Greptile Summary

This PR adds HMAC-SHA256 signing to all outbound webhook deliveries. Each PolydockStoreWebhook now carries an auto-generated secret (hidden from JSON/API responses), and the delivery job encodes the body once, signs it, then sends exactly those bytes — ensuring the signature and the transmitted payload can never diverge.

  • A creating boot hook mints a 40-character random secret for every new webhook, and the migration backfills one per existing row; signPayload() throws defensively if the secret is somehow absent.
  • json_encode now uses JSON_THROW_ON_ERROR so an encoding failure surfaces as a catchable exception rather than silently POSTing an empty body.
  • Documentation and a new feature test cover the header shape, constant-time comparison guidance, and end-to-end signature correctness.

Confidence Score: 5/5

Safe to merge — the signing logic is sound, previous feedback on empty-key and json_encode failures has been fully addressed, and the secret is correctly hidden from API responses.

All three layers of defense work together correctly: the creating hook mints a secret for new webhooks, the migration backfills existing rows with unique per-row secrets, and signPayload() throws before emitting a forged signature. The body is encoded once and reused for both the HMAC and the HTTP payload, eliminating any chance of signature/body mismatch. The new feature test verifies the end-to-end signature contract.

The migration file is the only one worth a second look — the secret column stays nullable in the schema after backfill, so the NOT NULL invariant is enforced purely at the application layer.

Important Files Changed

Filename Overview
app/Jobs/ProcessPolydockStoreWebhookCall.php Body encoded once with JSON_THROW_ON_ERROR and reused for both HMAC signing and the HTTP payload; signature header added correctly.
app/Models/PolydockStoreWebhook.php Auto-generates secret on create, hides it from serialization, and throws on empty secret before signing — all defensive layers are in place.
database/migrations/2025_07_01_000000_add_secret_to_polydock_store_webhooks_table.php Adds nullable secret column and backfills each existing row with its own random secret; column stays nullable in the schema — only the application layer enforces non-null.
tests/Feature/Jobs/ProcessPolydockStoreWebhookCallTest.php New test verifies the HMAC header matches the transmitted body and confirms the secret is hidden from toArray(); existing tests are unaffected.
docs/WEBHOOKS.md New documentation clearly describes the request shape, signing algorithm, and constant-time verification with a working PHP example.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Q as Queue
    participant J as ProcessPolydockStoreWebhookCall
    participant M as PolydockStoreWebhook
    participant R as Remote Endpoint

    Q->>J: dispatch(webhookCall)
    J->>J: json_encode(payload, JSON_THROW_ON_ERROR)
    J->>M: signPayload(body)
    alt secret is empty
        M-->>J: throw RuntimeException
        J->>J: catch → update status, rethrow → retry
    else secret present
        M-->>J: "sha256= + hash_hmac(sha256, body, secret)"
    end
    J->>R: "POST url / withBody(body) / X-Polydock-Signature: sha256=…"
    R-->>J: HTTP response
    alt 2xx
        J->>J: update status → SUCCESS
    else non-2xx
        J->>J: update status → FAILED/PENDING, throw → retry
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Q as Queue
    participant J as ProcessPolydockStoreWebhookCall
    participant M as PolydockStoreWebhook
    participant R as Remote Endpoint

    Q->>J: dispatch(webhookCall)
    J->>J: json_encode(payload, JSON_THROW_ON_ERROR)
    J->>M: signPayload(body)
    alt secret is empty
        M-->>J: throw RuntimeException
        J->>J: catch → update status, rethrow → retry
    else secret present
        M-->>J: "sha256= + hash_hmac(sha256, body, secret)"
    end
    J->>R: "POST url / withBody(body) / X-Polydock-Signature: sha256=…"
    R-->>J: HTTP response
    alt 2xx
        J->>J: update status → SUCCESS
    else non-2xx
        J->>J: update status → FAILED/PENDING, throw → retry
    end
Loading

Comments Outside Diff (1)

  1. app/Models/PolydockStoreWebhook.php, line 18-22 (link)

    P2 secret not in $fillable and no way to regenerate it

    The auto-generated secret is set via a direct property assignment in the creating hook, which bypasses $fillable and works correctly on creation. However, there is no supported path to rotate a webhook secret after creation — neither via mass-assignment (blocked) nor via a dedicated method. If a secret needs to be rotated (e.g., after a suspected leak), the only option is a raw database update. Consider adding a rotateSecret() method to make intentional rotation explicit and auditable, especially given the LogsActivity trait is already in use.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (3): Last reviewed commit: "feat: sign outbound webhooks with hmac-s..." | Re-trigger Greptile

Comment thread app/Jobs/ProcessPolydockStoreWebhookCall.php Outdated
@dan2k3k4 dan2k3k4 force-pushed the advisor/007-sign-outbound-webhooks-hmac branch 3 times, most recently from aa8ef37 to 13db360 Compare July 2, 2026 11:12
@dan2k3k4 dan2k3k4 force-pushed the advisor/007-sign-outbound-webhooks-hmac branch from 13db360 to e7ab879 Compare July 2, 2026 17:13
@dan2k3k4 dan2k3k4 merged commit b7d6c62 into dev Jul 2, 2026
4 checks passed
@dan2k3k4 dan2k3k4 deleted the advisor/007-sign-outbound-webhooks-hmac branch July 2, 2026 17:20
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