Looking to hire Laravel developers? Try LaraJobs

laravel-webhook-signature maintained by cboxdk

Description
Cbox Webhook Signature — verification-only webhook signature checking for Laravel, with drivers for GitHub, Stripe, Slack, Shopify, Standard Webhooks, Twilio, Mailgun and Postal. No migrations, no queue, no models. Secret rotation, replay protection and send-time outbound signing included.
Author
Last update
2026/08/05 12:28 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

Cbox Webhook Signature

Webhook signature verification for Laravel, with drivers for the providers you actually receive from — and outbound signing that uses the same code.

ci license

composer require cboxdk/laravel-webhook-signature

Receiving

// config/webhook-signature.php
'endpoints' => [
    'github' => ['scheme' => 'github', 'secrets' => [env('GITHUB_WEBHOOK_SECRET')]],
],
Route::post('/webhooks/github', GithubController::class)
    ->middleware('webhook.signature:github');
$webhook = VerifiedWebhook::fromRequestOrFail($request);

$webhook->json();               // the payload that was actually signed
$webhook->idempotencyKey();     // the provider's event id
$webhook->secretId;             // which secret verified — watch this during a rotation

Sending

Http::webhookSignature('outbound')->post($subscriber->url, $payload);

Signed by Guzzle middleware at send time, so the bytes that are signed are the bytes that are sent. No encoded body to keep in a variable and hand to two places.

Bundled schemes

Name Signed bytes Digest Encoding
github raw body SHA-256 hex
stripe {timestamp}.{body} SHA-256 hex
slack v0:{timestamp}:{body} SHA-256 hex
shopify raw body SHA-256 base64
standard-webhooks {id}.{timestamp}.{body} SHA-256 base64
twilio URL + sorted parameters SHA-1 base64
mailgun {timestamp}{token} SHA-256 hex
postal raw body (RSA, public key) SHA-256 / SHA-1 base64
cbox {timestamp}.{body} SHA-256 hex

Anything else: describe it in config with the generic HMAC driver, or register a class.

What it does that a hand-rolled verifier does not

Secret rotation. You cannot rotate a webhook secret atomically — sender and receiver deploy separately — so an endpoint holds a set of live secrets, and reports which one verified so you know when the old one can safely go.

Replay defence. Timestamp binding where the provider supports it, plus optional single-use enforcement against a shared store. Off by default, because a guard against a per-node cache reports success while enforcing nothing.

Typed failure reasons. "Our secret is missing" is distinguishable from "someone sent a bad signature" — in your logs, and in the status code: 401 for the caller's mistake, 500 for ours, so a provider retries a misconfigured receiver rather than discarding events.

The provider details. Stripe sends several valid signatures during a rotation. The Standard Webhooks secret is base64 behind a whsec_ prefix. Shopify is base64 where the others are hex. Twilio signs the URL, sorted byte-wise, with SHA-1. GitHub's legacy SHA-1 header is refused rather than accepted alongside SHA-256, and Postal's SHA-256 header — when present — is the only one checked, so a corrupted strong signature cannot be downgraded to the weak one.

Testing helpers that sign with the production code path:

$this->fakeWebhookEndpoint('github', 'github', 'test-secret');

$this->postSignedWebhook('/webhooks/github', 'github', ['action' => 'opened'])->assertOk();
$this->postUnsignedWebhook('/webhooks/github', ['action' => 'opened'])->assertUnauthorized();

What it deliberately is not

No migrations, no models, no queued jobs, no webhook_calls table. Storing deliveries and processing them are decisions your application has already made; a library that made them again would be something to fit around rather than something to drop in.

Conformance

Every bundled scheme is tested against a signature produced by an implementation other than this one — a round trip cannot catch a misread specification, because both halves are wrong the same way.

github, slack, standard-webhooks and twilio use the worked examples their providers publish; the HMAC primitive is checked against RFC 4231 vectors. stripe, shopify, mailgun, postal and cbox are checked against signatures generated by the provider's own SDK, by the openssl CLI, or by the independently written signer already deployed in the Cbox billing service. docs/security/conformance.md names the source for each.

Nothing here is hand-rolled cryptography: MACs use PHP's hash_hmac() and hash_equals() in one class, and the asymmetric path uses openssl_verify() in another.

Requirements

PHP 8.4+, Laravel 12 or 13. See docs/requirements.md.

Documentation

docs/index.md — quickstart, per-provider recipes, rotation, replay protection, extension points, threat model.

Development

composer qa    # pint, phpstan (level max), pest, license check, security audit

License

MIT. See LICENSE.