Under the hood of ASP.NET Core WebHooks - Verification Requests
This is third part of my Under the hood of ASP.NET Core WebHooks series:
- Introduction
- Routing
- Verification Requests
- Signature Validation
- Model Binding
I was expecting this post subject (verification requests) to be plain and simple, yet it managed to surprise me.
What's verification request about
Some webhooks perform a verification request as part of creating a subscription. This request is typically a GET (while notifications are POSTs) with a dedicated query parameter which serves as challenge. The goal is to verify that the provided URL in fact supports the webhook. To confirm that, the application is expected to echo the challenge parameter value in response body. Dropbox webhook is textbook example of this flow.
What's in the box for verification request
ASP.NET Core WebHooks provides support for verification requests through WebHookGetHeadRequestFilter
. The filter is "triggered" when IWebHookGetHeadRequestMetadata
interface is implemented by metadata. I wanted to see if it provides what I needed for my WebSub compliant receiver so I've added it to metadata, which enforced implementation of three properties.
public class WebSubMetadata : WebHookMetadata, IWebHookGetHeadRequestMetadata, ...
{
...
public bool AllowHeadRequests => false;
public string ChallengeQueryParameterName => "hub.challenge";
public int SecretKeyMinLength => 0;
}
The AllowHeadRequests
and ChallengeQueryParameterName
are self-explanatory. As I didn't care about any keys at this stage I've decided to set SecretKeyMinLength
to zero. I've run the demo application, fired the prepared request from Postman and (to my surprise) received a 500 response. I've quickly looked through the logs and found this:
System.InvalidOperationException: Could not find a valid configuration for the 'websub' WebHook receiver. Configure secret keys for this receiver.
But I didn't want any secret keys...
Further examination of WebHookGetHeadRequestFilter
revealed that responding to verification requests isn't its only responsibility. It also confirms that secret key is properly configured for given receiver and not having one is not an option. This is surprising as secret keys are not related to verification requests but signature verification (hopefully the subject of next post in this series). Regardless, I had to configure one.
The WebHookGetHeadRequestFilter
is looking for secret key under WebHooks:{webHookReceiver}:SecretKey:{id}
configuration key. If the webhook URL doesn't contain id, the value default
will be used in its place. This means that scenarios where all webhooks URLs are not known upfront is becoming challenging and requires smart usage of in-memory configuration provider or implementation of custom configuration provider.
For testing purposes I've put a dummy value into configuration file and received the response I wanted.
What if that's not what you need
The secret key requirement is a pain, but for most cases it's bearable. What if you have special requirements (which ended up being the case for my WebSub receiver)? The WebHookGetHeadRequestFilter
doesn't provide way for customization, so the only solution seems to be using IWebHookFilterMetadata
(which was briefly described in the introduction post) and creating your own filter. The ASP.NET Core WebHooks helps a little bit here by exposing static Order
property on every filter, which allows placing the equivalent in the right spot. The filter should implement IResourceFilter
or IAsyncResourceFilter
.
public class WebSubWebHookIntentVerificationFilter : IAsyncResourceFilter, IOrderedFilter
{
...
public int Order => WebHookGetHeadRequestFilter.Order;
...
}
This gives full freedom in handling verification requests.