Receiving GitHub Webhooks When Using the ASP.NET Core Developer Certificate

For the last couple of weeks, I have been experimenting with creating a GitHub App using .NET. I’ve been pursuing this because I have an idea for an integration that could be interesting, and it also provides a real-world scenario to keep my skills fresh. I get to explore some implementation details of ASP.NET Core that I find interesting and want to learn more about. In addition, I am taking the opportunity to dogfood some of the work I’ve been doing at Elastic around OpenTelemetry and instrumentation of .NET applications. I need to experience the pain points and gaps in the documentation, which I can then help address.

One of my early goals has been to support the receipt of GitHub webhooks in my ASP.NET Core application. In my next post(s), I plan to discuss the code for a middleware component that can handle webhook requests from GitHub, applying best practices and high-performance code.

In this post, I want to lay a foundation by sharing the essential early steps to enable testing receipt of GitHub webhooks while developing locally using HTTPS with the self-signed ASP.NET Core developer certificate.

GitHub Webhooks

Webhooks provide a way for services to notify integrations about events that occur by sending predefined payloads to an endpoint in your web application. In the world of GitHub, a GitHub App can register an interest in any number of events relating to pretty much anything that happens in GitHub. You can view their extensive documentation for details of the various GitHub webhooks and payloads.

The challenge with developing a new GitHub application is that we need a way to test webhooks in our local development environment. One option would be to define integration tests that send fake payloads to our webhook handling endpoint. However, some aspects of securing the webhooks endpoint are not easy to test, and at some point, we want to do some proper integration testing to allow GitHub to send us actual requests. So, how do we do that when our application runs locally and is not exposed to the internet?

Fortunately, GitHub has pretty good documentation and includes a page that describes how to test GitHub webhooks. Most of what you must do is described in depth, so I’ll only summarise the steps in this post. Check out the GitHub documentation for the finer points and some explanations. There is one crucial aspect that isn’t covered and that you are likely to run into. ASP.NET Core web applications created from the templates now include HTTPS support by default. To enable this in development, ASP.NET Core uses a self-signed certificate. I’ll explain why this introduces a minor hurdle we must overcome.

NOTE: My development environment is Windows, and this post describes some steps specific to the Windows OS. That said, reproducing the relevant steps on another OS shouldn’t be too challenging.

Prerequisites

GitHub App Registration

Before you can begin development, you’ll need to register a GitHub app. This can be created under the settings for your GitHub account. I won’t go through all the options here as I assume you already know how to create this if you’re ready to begin using webhooks. For more information, please visit the GitHub document covering creating GitHub Apps.

The crucial part is to scroll down and provide your unique smee.io URL as the Webhook URL. GitHub will send webhook payloads to this URL, and the smee client will pick those up and forward them to your local ASP.NET Core application.

Screenshot of the Webhook section of the GitHub App creation form allowing the registration of a webhook URL.

NOTE: We’re not configuring a secret in this post to keep things simple, but you absolutely should not skip this for real applications. In an upcoming blog post, I’ll describe how to use the secret to validate and authenticate GitHub webhook payloads.

ASP.NET Core HTTPS Development Certificate

By default, when running an ASP.NET Core application for the first time, the .NET SDK will attempt to install a dev certificate if one does not already exist. This certificate is used during development to support running a HTTPS secured website. If, for any reason, this isn’t set up, you can manually create a certificate using the dotnet dev certs command line tool.

A smee.io URL and the smee-client

Visit smee.io and start a new channel, which will generate a unique URL for you. You will also require the smee client, which requires Node.js and can be installed via NPM. Once installed, we can use the client to receive and forward webhooks from GitHub into a URL on our application running under localhost.

ASP.NET Core Application

We’ll also need an ASP.NET application running locally to receive webhooks from GitHub. Before we can set up webhook forwarding to our locally running application, we need to add the bones of a middleware that will handle requests to our webhook endpoint. I won’t provide any implementation in this post, but I will discuss that in an upcoming blog entry. Don’t forget to subscribe to receive updates via email!

namespace GitHubAppSample;

public class GitHubWebHookMiddleware(RequestDelegate next)
{
    private readonly RequestDelegate _next = next;

    private const string WebHookPath = "/github/webhook";

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments(WebHookPath, StringComparison.Ordinal) && context.Request.Method == "POST")
        {
            // TODO
        }

        await _next(context);
    }
}

The preceding code creates a middleware that will handle requests to our application’s webhook URL, which is, in this example, “/GitHub/webhook.” We further filter to handle only POST requests, the expected HTTP method GitHub will use.

In the program file for the application, we can add the middleware to the request processing pipeline.

app.UseMiddleware<GitHubWebHookMiddleware>();

The Problem with HTTPS

We’re ready to test whether we can receive a webhook at this stage by adding a breakpoint in our middleware code and then debugging the application. We’ll need the HTTPS URL you can retrieve from the console log messages or the launchSettings.json file. In my case, the HTTPS port used on my local machine is 7192.

With the application running, we can launch the smee client to start forwarding webhooks to our application. We can use the following command.

smee --url https://smee.io/PFxa5d1TmwfmwYR --target https://localhost:7192/github/webhook

Don’t forget to replace the URL with your unique URL from smee.io! We specify the full URL for our webhook endpoint as the target.

When we added the webhook URL to GitHub.com and saved the new GitHub App, a ping event will have been dispatched to the configured URL. We can visit our unique smee.io URL to see the delivered events and trigger them to redeliver to the smee client and onto our local application.

Screenshot from smee.io showing how to redeliver the ping webhook event.

At this point, we expect our breakpoint in the middleware to be hit. Unfortunately, it won’t be, and it might not be obvious why. If we check the terminal where we launched the Smee client, we will get a clue pointing to the root cause.

TypeError: fetch failed
    at node:internal/deps/undici/undici:13185:13
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async Client.onmessage (C:\Users\SteveGordon\AppData\Roaming\npm\node_modules\smee-client\index.js:54:30) {
  [cause]: Error: self-signed certificate
      at TLSSocket.onConnectSecure (node:_tls_wrap:1679:34)
      at TLSSocket.emit (node:events:520:28)
      at TLSSocket._finishInit (node:_tls_wrap:1078:8)
      at ssl.onhandshakedone (node:_tls_wrap:864:12) {
    code: 'DEPTH_ZERO_SELF_SIGNED_CERT'
  }
}

The problem is that the Smee client, built on Node.js, sees that our application uses HTTPS, but the certificate is self-signed and, therefore, untrusted by default.

Before proceeding, stop the smee client using CTRL + C.

Trusting the ASP.NET Core Development Certificate

When the .NET SDK creates our self-signed certificate for ASP.NET Core, it will be registered with the Windows certificate store. We can double-check this by opening the Certificate Manager. Press the Windows Key + R to open the Run dialog and type “certmgr.msc”.

Windows Run dialog window configured to open "certmgr.msc".

We will find the certificate, issued to “localhost”, listed under “Personal > Certificates”.

Screenshot of the Windows Certificate Manager UI expanded to show the personal certificates, including the ASP.NET Core HTTP development certificate issued to localhost.

We need to export this certificate in the correct format as our next step, by right-clicking on the entry and choosing “All Tasks > Export…”.

Screenshot of the Windows Certificate Manager UI context menu for exporting the ASP.NET Core developer certificate.

The Certificate Export Wizard will launch. We can accept most of the defaults to export without the private key.

Ensure you choose the Base-64 encoded X.509 (CER) format when selecting the export format.

And finally, choose a location for your certificate to be exported.

With the export complete, we must instruct Node.js to trust our self-certificate. We can do this by setting one environment variable. I use a PowerShell terminal, so I can set this temporarily for my session. If you prefer, consider setting this permanently as a system environment variable. 

$Env:NODE_EXTRA_CA_CERTS = "c:\certs\aspnetcore.cer" 

We can now relaunch the smee client using the same command used before.

smee --url https://smee.io/PFxa5d1TmwfmwYR --target https://localhost:7192/github/webhook

After this starts, we can return the smee.io website to redeliver the ping request. This time, the breakpoint in our middleware will be hit, and when we visit the terminal where the Smee client was launched, we no longer see an error message.

Forwarding https://smee.io/PFxa5d1TmwfmwYR to https://localhost:7192/github/webhook
Connected https://smee.io/PFxa5d1TmwfmwYR
POST https://localhost:7192/github/webhook - 200

The ping request was successfully forwarded to our application running on our localhost.

Summary

In this post, I have demonstrated how to use smee.io and the Smee client to deliver GitHub webhook events to an ASP.NET Core application under development via its HTTPS URL. To achieve this, the critical step was to export the ASP.NET Core development certificate in the Base64 encoded format from the Certificate Manager in Windows. Once exported, we could instruct Node.js to trust the certificate and, therefore, ensure that the Smee client could forward requests to our HTTPS URL.

In an upcoming post, I will show the steps to build a secure endpoint for correctly validating and handling GitHub webhooks. To get updates about new blog posts, subscribe to this blog by email or follow me on Twitter.

Related Content


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 7x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon