How to robustly expose a webhook in Laravel
Let's create a Laravel client app that can expose a webhook and take related action
In this tutorial, we will see how to make our Laravel application capable of receiving notifications from a server app, using a simple webhook.
The app that exposes the webhook is a client app. The client part is generally much simpler than the server part. In fact, the client basically has to worry about exposing an endpoint (i.e. the webhook) and managing incoming calls.
In reality, in a robust and efficient system, the client should respond immediately and without delay. For this purpose, it is necessary to queue the received notifications and subsequently manage them with a Job. By doing so, it is also possible to perform complex and/or time-consuming tasks, without blocking the caller unnecessarily.
Thankfully, to manage all of this we don't have to rewrite everything from scratch. We will use the package: π Laravel Webhook Client
The package is developed and maintained by Spatie and we all know how much this is a guarantee!
Can I trust the caller?
One of the main issues to be addressed on the client side concerns the reliability of the caller, namely: is the application that contacts our webhook exactly the one authorized to do so or is there some funny guy pretending to be it?
The method that we will use in this tutorial is to verify that a signature is present among the headers of the requests received and that this has been affixed using a secret key. The package will do the checking for us. It will be enough for us to configure the correct Signature Secret Key.
By default, Laravel Webhook Client package uses the class DefaultSignatureValidator
to validate signatures. This is how that class will compute the signature:
hash_hmac('sha256', $request->getContent(), $signatureSecretKey);
As you can see, the validation is very easy but, at the same time, very strong.
Steps
1. Install the Laravel Webhook Client package
Install the package:
composer require spatie/laravel-webhook-client
Publish the config file config/webhook-client.php
:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
Publish the migration:
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
Migrate:
php artisan migrate
2. Open the route and handle the job
Add a route in routes/web.php
.
// routes/web.php
Route::webhooks('/webhook');
// this will open a POST route `/webhook`
IMPORTANT: Of course, you can change /webhook
with others, but the correct URL must be shared with the server app.
Now, if you run
php artisan route:list
you can check if there is the POST entry:
Because the app that sends webhooks to you has no way of getting a csrf-token, you must add that route to the except
array of the VerifyCsrfToken
middleware:
// app/Http/Middleware/VerifyCsrfToken.php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
protected $except = [
'/webhook', // <-- add this line
];
}
Now, it's time to set in the .env
file the Signature Secret Key that your server application shared with you:
# .env
# ...
WEBHOOK_CLIENT_SECRET="paste-here-the-signature-secret-key-shared-with-server-app"
If you make a test request now, you get an Exception by the package informing you that there is no valid process webhook job class:
And that's what we're going to do now!
Make the job to handle calls:
php artisan make:job ProcessWebhookJob
For the purpose of the tutorial, we simply log the received data. We can find it in $this->webhookCall
:
// app/Jobs/ProcessWebhookJob.php
namespace App\Jobs;
/* use ... */
use Spatie\WebhookClient\Jobs\ProcessWebhookJob as SpatieProcessWebhookJob;
class ProcessWebhookJob extends SpatieProcessWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
// `$this->webhookCall` contains an instance of:
// `\Spatie\WebhookClient\Models\WebhookCall`
$event = \Arr::get($this->webhookCall->payload, 'event');
$data = \Arr::get($this->webhookCall->payload, 'data', []);
// ...
// [perform the work here] ...
// simply log `event` and `data`:
logger('ProcessWebhookJob', [
'event' => $event,
'data' => $data,
]);
// ...
}
}
Finally, we set the process_webhook_job
field to indicate the Job class we just created, in the config file at config/webhook-client.php
:
// config/webhook-client.php
// ...
'process_webhook_job' => \App\Jobs\ProcessWebhookJob::class,
Now, if you re-launch the test request from earlier, you successfully get an invalid signature error:
3. Make some tests from server app
π From the server app we trigger a test event:
π― In the log of client app we have the correct data!
π Another event triggered from server app:
π― And in the client app log:
4. Prune WebhookCall
models
With each call to our webhook, the package will add a record of the WebhookCall
model. You can simply prune old records, by calling model:prune
.
// app/Console/Kernel.php
namespace App\Console;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Spatie\WebhookClient\Models\WebhookCall;
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
$schedule->command('model:prune', [
'--model' => [WebhookCall::class],
])->daily();
// This will not work, as models in a package are not used by default
// $schedule->command('model:prune')->daily();
}
}
By default, records created more than 30 days ago will be removed.
You can customize this limit using the parameter delete_after_days
in file config/webhook-client.php
:
// config/webhook-client.php
return [
'configs' => [
// ...
],
'delete_after_days' => 30,
];
βΈ Enjoy your coding!
If you liked this post, don't forget to Subscribe to my newsletter!