Skip to content

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.