Validating A GitHub Webhook Request With AWS Lambda

ยท 819 words ยท 4 minute read
Learn how to validate GitHub webhook requests with AWS Lambda and Go

Introduction ๐Ÿ”—

Webhooks allow you to build or set up integrations, which subscribe to certain events on GitHub. Anytime such an event occurs, an HTTP request is triggered with a payload, containing information about the event. The HTTP (POST) request will be sent to an endpoint of your choice. With the payload information all sorts of processing can happen, such as starting a CI/CD build, updating a database, running a piece of code etc.

Prerequisites ๐Ÿ”—

This post assumes that you have basic AWS knowledge and that you are familiar with AWS tooling such as AWS CDK and AWS CLI. Basic Golang knowledge is also beneficial.

Validation ๐Ÿ”—

It is highly recommended securing any webhook request of a GitHub repository via a secret token. Otherwise, your code could be triggered by any HTTP request directed at your endpoint and would consume unnecessary compute time or could introduce security issues.

The general idea is to calculate a hash, using the secret token and the request body, then compare the result to the hash of the GitHub request. The hash is found in the request header with the key X-Hub-Signature-256. GitHub uses an HMAC Hex digest to compute the hash, so with Go we can use the standard library:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    ...
)

However, using the plain == operator for comparing hashes is not recommended, because it is prone to certain timing attacks. Hence, we use hmac.Equal, which compares two MACs for equality without leaking timing information. Note the following code to validate the payload:

const (
	signatureSeparator = "="
	numberOfParts      = 2
)

// ValidatePayload checks if the payload's calculated hash is equal to given signature.
func ValidatePayload(secret, signature string, payload []byte) (bool, error) {
	sigParts := strings.SplitN(signature, signatureSeparator, numberOfParts)
	if len(sigParts) != numberOfParts {
		return false, errors.New("failed to parse signature")
	}

	decoded, err := hex.DecodeString(sigParts[1])
	if err != nil {
		return false, errors.Wrap(err, "failed to decode signature")
	}

	calculated := calcHash(secret, payload)
	return hmac.Equal(calculated, decoded), nil
}

// calcHash computes the hash of payload's body according to the webhook's secret token
// see https://developer.github.com/webhooks/securing/#validating-payloads-from-github
func calcHash(secret string, payload []byte) []byte {
	hm := hmac.New(sha256.New, []byte(secret))
	hm.Write(payload)
	return hm.Sum(nil)
}

The GitHub signature is Hex encoded. We need to decode it before trying to compare the hashes. You can find the full Go example on GitHub.

Infrastructure ๐Ÿ”—

Now that we have the Go code ready for validating the GitHub request, we can set up the infrastructure. With the AWS CDK, we can easily define and create AWS infrastructure as code in one of three popular programming languages (TypeScript, Python and Java). I assume you already have your setup ready, but if not, AWS offers a good introduction.

We need an API Gateway to accept incoming requests. The API Gateway wraps a request into an API Gateway Proxy Request and invokes our Go Lambda function:

Webhook infrastructure
Webhook infrastructure

In our example, after the request has been successfully validated, the Lambda publishes a message into an SQS queue. But of course, it is up to you what happens after validation. You can find the full CDK code for setting up the infrastructure on GitHub. Just replace the repositoryId constant in the webhook-blog-infra-stack.ts file with your GitHub repository ID. Additionally, we clone the mentioned Go Lambda in the same parent folder as the infrastructure project:

/ ...
  |- code
    |- drizzle-webhook
    |- webhook-infra

To deploy our CDK stack type cdk synth and afterwards cdk deploy in your favorite console at the root of the infrastructure project. After execution, we will receive our API Gateway endpoint URL in the console output of the command:

CDK console out
CDK console API endpoint URL

Note the WebhookBlogInfraStack.webhookAPIURL. CDK also creates a secret with the AWS Secrets Manager. After deploying our stack, we can fetch the secret via AWS CLI:

aws secretsmanager get-secret-value --secret-id webhook-secret

Alternatively we can get it via the AWS (Web-) Console. The Lambda codes tries to fetch the secret based on the repository ID of the incoming webhook request. If a secret exists, it tries to validate the request with the given secret.

GitHub Configuration ๐Ÿ”—

With the API Gateway endpoint from the CDK output we are now able to configure the webhook in GitHub. Go to the repository, which ID you used to create the secret, to Settings and select Webhooks.

GitHub webhook configuration
GitHub webhook configuration

We fill in the API Gateway endpoint URL and paste our secret. Also, we set the content type to application/json, since this is what we expect and want to parse in the Lambda. After a click on the save button we are good to go.

Conclusion ๐Ÿ”—

Any change in the repository will now trigger a POST request to our API Gateway endpoint and the Lambda will try to validate it. If the execution was successful, we can see it immediately on the GitHub webhook page. Of course instead of publishing to SQS, we could do all sorts of things with the incoming request.