Installation
Installation
composer require cboxdk/laravel-webhook-signature
The service provider and the Webhooks facade alias are registered by package discovery.
Nothing else runs on install: there are no migrations to publish and no tables to create.
Publish the configuration
php artisan vendor:publish --tag=webhook-signature-config
That writes config/webhook-signature.php. Until you declare an endpoint there, the
package does nothing — every lookup is deny-by-default, so an unconfigured endpoint name
throws rather than resolving to something permissive.
Declare an endpoint
'endpoints' => [
'github' => [
'scheme' => 'github',
'secrets' => [env('GITHUB_WEBHOOK_SECRET')],
],
'stripe' => [
'scheme' => 'stripe',
'secrets' => [env('STRIPE_WEBHOOK_SECRET')],
'tolerance' => 300,
],
],
One endpoint per sender, not per provider. Two GitHub organisations, or Stripe's live and test modes, are separate endpoints with separate secrets and separate rotation timelines.
Protect the route
Route::post('/webhooks/github', GithubController::class)
->middleware('webhook.signature:github');
Middleware ordering matters
The signature covers the raw request body. Any middleware that reads and rewrites the body before verification — payload normalisers, some request-logging packages — changes those bytes, and verification will then fail for every delivery with a symptom that points nowhere near the cause.
Put webhook routes in a route file that does not apply those, and keep
webhook.signature early in whatever group they do get.
CSRF
Webhook routes must be exempt from CSRF verification. In a default Laravel application
routes/api.php already is; a route in routes/web.php needs an explicit exclusion.
Read the result
use Cbox\WebhookSignature\ValueObjects\VerifiedWebhook;
$webhook = VerifiedWebhook::fromRequestOrFail($request);
$webhook->json(); // the payload that was actually signed
$webhook->eventId; // the provider's delivery id, when it sends one
$webhook->secretId; // which configured secret verified it
$webhook->timestamp; // when the sender signed it, for schemes that bind one
fromRequestOrFail() throws when the route was not verified, which has exactly one cause:
the middleware is not on the route. That is a wiring mistake rather than a runtime
condition, so making it impossible to proceed by omission is worth more than a null a
controller can forget to check.
fromRequest() returns null for the same case, for the rare caller that genuinely handles
it.
Verify without the middleware
For a controller that needs to verify something other than the current request — a delivery pulled off a queue, a stored payload being reprocessed:
use Cbox\WebhookSignature\Facades\Webhooks;
use Cbox\WebhookSignature\ValueObjects\WebhookMessage;
$verified = Webhooks::verify('github', WebhookMessage::make($body, $headers));
It throws SignatureVerificationFailed rather than returning false, so there is no return
value to forget to check.