Laravel cookbook · Intermediate
Verify incoming webhook signatures
Anyone can POST fake events to the webhook endpoint.
Approach
Verify an HMAC signature over the raw body with a constant-time comparison and a timestamp tolerance in a route middleware, exclude the route from CSRF, and process events idempotently by event ID.
Solution
<?php
// app/Http/Middleware/VerifyWebhookSignature.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VerifyWebhookSignature
{
public function handle(Request $request, Closure $next): Response
{
$timestamp = (int) $request->header('X-Webhook-Timestamp');
$signature = (string) $request->header('X-Webhook-Signature');
$expected = hash_hmac('sha256', $timestamp.'.'.$request->getContent(), config('services.payments.webhook_secret'));
abort_unless(abs(time() - $timestamp) <= 300, 401, 'Stale webhook');
abort_unless(hash_equals($expected, $signature), 401, 'Invalid signature');
return $next($request);
}
}
<?php
// routes/web.php
use App\Http\Controllers\PaymentWebhookController;
use App\Http\Middleware\VerifyWebhookSignature;
use Illuminate\Support\Facades\Route;
Route::post('/webhooks/payments', PaymentWebhookController::class)->middleware(VerifyWebhookSignature::class);
// bootstrap/app.php
// ->withMiddleware(fn ($middleware) => $middleware->validateCsrfTokens(except: ['webhooks/*']))
// In the controller: WebhookEvent::firstOrCreate(['provider_event_id' => $request->input('id')], [...])
// and only process the event when it was newly created.
Pitfalls
- Signing
json_encode($request->all())instead of the raw body. ==comparison instead ofhash_equals.- Doing slow work in the webhook request; store the event and process it in a queued job.
Variations
- Use the provider's SDK verification helper when available (for example Stripe's
Webhook::constructEvent). spatie/laravel-webhook-clientpackages verification, storage and queued processing.
Further reading: Middleware
Reference: https://laravel.com/docs/13.x/middleware#defining-middleware
More Laravel recipes
- Authorize actions with policies
- Cache expensive data with stale-while-revalidate
- Call external APIs with timeouts, retries and fakes
- Catch N+1 queries with strict Eloquent mode
- Delete old records automatically with prunable models
- Dispatch events and listeners after the transaction commits
- Issue scoped API tokens with Sanctum
- Make queued jobs reliable with retries, backoff and uniqueness