DEV Community

ryan
ryan

Posted on

How To Validate GitHub Webhooks With Laravel and PHP

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.

GitHub Webhook Form

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.

Conclusion

Link to this repository on GitHub

Oldest comments (2)

Collapse
 
dels07 profile image
Deli Soetiawan

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)

Collapse
 
fractalbit profile image
fractalbit

Very useful, thank you!