How To Validate GitHub Webhooks With Laravel and PHP
Introduction
GitHub webhooks are sent to your endpoint when events occur in a repository. This is a powerful extensiblity feature that provides the developer a platform for many use cases such as push notifications and automatically deploying code when a repository has new commits. As security aware developers, we should always validate our incoming webhooks against a known token.
In this post, I'll show how to properly validate webhooks from GitHub. When we're done, you can use this code to validate your own hooks.
At the end you'll have an API endpoint in the form of
https://example.com/api/webhooks/github/handle that's validated by a method with the following signature:
protected function validateGithubWebhook($known_token, Request $request);
Prerequisites
Before you begin this guide you'll need the following:
- A repository on GitHub
- A public server that can accept HTTP requests from GitHUb
- Laravel application
If you don't have a public server available, then you can sign up for a trial at Amezmo and deploy your application in seconds without having to setup Nginx, or PHP.
Step 1 β Generate a Secret Token
First, let's open up a shell and generate a random token that we'll use for our GitHub webhook:
openssl rand -hex 32
With the above command executed, copy the value into your .env
file replacing the with the result of your command.
GITHUB_WEBHOOK_SECRET=test
After you've wired up the .env file, be sure to define the configuration entry in config/app.php
. Add the following entry to the array.
'github_webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
Step 2 - Define the API Route in Laravel
Finally, let's define our API endpoint in our Laravel application. This endpoint will be an API endpoint type that will not set a session cookie. Since this endpoint will not authenticated against a user session, we do not need to set a cookie in the HTTP response.
Open up routes/api.php
and define a new route group
Route::namespace('Webhooks')->prefix('/webhooks')->group(function () {
Route::post('/github/handle', 'GithubWebhookProcessor@handle');
});
We've defined a route group with the name Webhooks. This allows us to stay generic while providing a path
to extensibility in case we want to add other webhook providers in the future.
Step 3. Create a webhook on GitHub.
Using any repository that you have on GitHub, create a webhook from the repository settings page.
Our webhook will be a JSON payload with just the push event for now. Use the token we generated in the first step for the Secret. Replace example.com with your own domain.
Next, we'll define our folder and PHP file where we'll define our class to handle this route.
Step 4 β Compose the Webhook Class
Create the Webhooks folder under app/Https/Controllers
. Under our new Webhooks
folder, define our new class in GithubWebhookProcessor.php
.
<?php
namespace App\Http\Controllers\Webhooks;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
class GithubWebhookProcessor
{
/** @var \Psr\Log\LoggerInterface */
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Validate an incoming github webhook
*
* @param string $known_token Our known token that we've defined
* @param \Illuminate\Http\Request $request
*
* @throws \BadRequestHttpException, \UnauthorizedException
* @return void
*/
protected function validateGithubWebhook($known_token, Request $request)
{
if (($signature = $request->headers->get('X-Hub-Signature')) == null) {
throw new BadRequestHttpException('Header not set');
}
$signature_parts = explode('=', $signature);
if (count($signature_parts) != 2) {
throw new BadRequestHttpException('signature has invalid format');
}
$known_signature = hash_hmac('sha1', $request->getContent(), $known_token);
if (! hash_equals($known_signature, $signature_parts[1])) {
throw new UnauthorizedException('Could not verify request signature ' . $signature_parts[1]);
}
}
/**
* Entry point to our webhook handler
*
* @param \Illuminate\Http\Request $request
*
* @return mixed
*/
public function handle(Request $request)
{
$this->validateGithubWebhook(config('app.github_webhook_secret'), $request);
$this->logger->info('Hello World. The GitHub webhook is validated');
$this->logger->info($request->getContent());
}
}
Step 4 β Test it
We've wired everything up on our end, now we can test our webhook by creating a new commit in the repository where we defined our webhook.
Top comments (2)
Very useful, thank you!
Apart from underscored variable (which should be camel case, PSR-2) this is good tutorial, however you should explain for which case we need to make a webhook (example call git pull after master push)