How to robustly expose a webhook in Laravel

tonyjoe

Tony Joe Dev

Posted on September 6, 2023

How to robustly expose a webhook in Laravel

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!

Ā 

šŸ’” Note: Here we won't worry about what the server app has to do, but if you are interested you can refer to the post How to manage subscribed webhooks in Laravel.


Can I trust the caller?

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);
Enter fullscreen mode Exit fullscreen mode

As you can see, the validation is very easy but, at the same time, very strong.


Steps

  1. Install the Laravel Webhook Client package

  2. Open the route and handle the job

  3. Make some tests from server app

  4. Prune WebhookCall models


1. Install the Laravel Webhook Client package

Install the package:

composer require spatie/laravel-webhook-client
Enter fullscreen mode Exit fullscreen mode

Publish the config file config/webhook-client.php:

php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
Enter fullscreen mode Exit fullscreen mode

Publish the migration:

php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
Enter fullscreen mode Exit fullscreen mode

Migrate:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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:

Route list

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
    ];
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

Test request to webhook

And that's what we're going to do now!

Make the job to handle calls:

php artisan make:job ProcessWebhookJob
Enter fullscreen mode Exit fullscreen mode

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,
        ]);

        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
Enter fullscreen mode Exit fullscreen mode

Now, if you re-launch the test request from earlier, you successfully get an invalid signature error:

Test request to webhook


3. Make some tests from server app

šŸ‘‰ From the server app we trigger a test event:

Trigger event from server app

šŸ’” Note: Here we are using the server app that we created with the post How to manage subscribed webhooks in Laravel.

šŸŽÆ In the log of client app we have the correct data!

Logs in client app


šŸ‘‰ Another event triggered from server app:

Another event from server app

šŸŽÆ And in the client app log:

Logs again in client app


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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
];
Enter fullscreen mode Exit fullscreen mode

āœø Enjoy your coding!

Ā 

If you liked this post, don't forget to add your Follow to my profile!
šŸ’– šŸ’Ŗ šŸ™… šŸš©
tonyjoe
Tony Joe Dev

Posted on September 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related