Full Series links can be found here.
In this post I am going to look at how the webhooks secret value we setup in the previous post works, what it does and the webhooks process flow for AzureAlertWebHook.
User Secret values
Let’s recap from the previous post about what the user secrets value for the Webhook looks like:
{
"WebHooks": {
"azurealert": {
"SecretKey": {
"adamalert": "83699ec7c1d794c0c780e49a5c72972590571fd8"
}
}
}
}
There is a lot of nesting going on here but for valid reasons. There maybe a number of different types of web hooks being used as well as multiple instances of the same type of web hook waiting for various values to be sent.
Lets break the config down and see what which part represents
-
Webhooks
This is the high level configuration property for all the web hooks. This constant value is defined in the
WebHookConstants.ReceiverConfigurationSectionKey
property. -
azurealert
This is what is known as the Receiver Name. It gets registered through the
AzureAlertMetadata
constructor specifying it to the baseWebHookMetadata
class. This metadata class is registered at start up using theAddAzureAlertWebHooks()
middleware method. -
SecretKey
This defines the secret key section. The constant value is defined in the
WebHookConstants.SecretKeyConfigurationKeySectionKey
property. -
adamalert
This is the identification value of my specific alert. The value is the specific WebHookReceiverId which is defined in the route by
WebHookConstants.IdKeyName
route value.
So how does these map into a routed api call from our alert setup in Azure?
Mapping
https://<host>/api/webhooks/incoming/azurealert/adamalert?code=83699ec7c1d794c0c780e49a5c72972590571fd8
The receiver name is mapped to WebHookConstants.ReceiverKeyName
and the id is mapped from WebHookConstants.IdKeyName
constant values. The route is specified in the WebHookRoutingProvider
and is hard coded which specifies the template it is looking at.
private static string ChooseTemplate(IDictionary<string, string> routeValues)
{
var template = "/api/webhooks/incoming/"
+ $"{{{WebHookConstants.ReceiverKeyName}}}/"
+ $"{{{WebHookConstants.IdKeyName}?}}";
return template;
}
Interestingly the routeValues
are currently ignored which I found quite interesting. We will have to wait and see if this changes over time.
Submitting an alert
In the previous post I ran through posting a sample message to the api end point. As part of the url there is a code query string value which is used for verification.
Once submitting the alert message to the end point there is a pipeline of Microsoft.AspNetCore.WebHooks.Filters
which the process runs through. I won’t go into detail about these filters now but the important one we are looking for is the WebHookVerifyCodeFilter
.
On each of the filters registered there is an OnResourceExecuting
method which has the ResourceExecutingContext passed in. This method is executed in the order in which the Filters are specified.
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var routeData = context.RouteData;
if (routeData.TryGetWebHookReceiverName(out var receiverName) &&
_codeVerifierMetadata.Any(metadata => metadata.IsApplicable(receiverName)))
{
var result = EnsureValidCode(context.HttpContext.Request, routeData, receiverName);
if (result != null)
{
context.Result = result;
}
}
}
It tries to extract out the receiver name from the route data. If there is no receiver specified then there is no need to carry on. It also makes sure there is an applicable IWebHookVerifyCodeMetadata
registered which AzureAlertMetadata
is.
What does the IWebHookVerifyCodeMetadata
interface do? The source code does explain this pretty well …
/// <summary>
/// <para>
/// Marker metadata interface for receivers that require a <c>code</c> query parameter. That query parameter must
/// match the configured secret key. Implemented in a <see cref="IWebHookMetadata"/> service for receivers that do
/// not include a specific <see cref="Filters.WebHookSecurityFilter"/> subclass.
/// </para>
/// <para>
/// <see cref="Filters.WebHookVerifyCodeFilter"/> verifies the <c>code</c> query parameter based on the existence
/// of this metadata for the receiver. <see cref="Filters.WebHookReceiverExistsFilter"/> verifies at least one
/// receiver-specific filter exists unless this metadata exists for the receiver.
/// </para>
/// </summary>
public interface IWebHookVerifyCodeMetadata : IWebHookMetadata, IWebHookReceiver
{
}
So as you can see this marker interface is the way it specifies that the webhook is expecting the code query string parameter.
Validating the code querystring parameter
Now we know this implementation is expecting a code querystring parameter we need to make sure it is valid. This is the job of the EnsureValidCode
method in the WebHookVerifyCodeFilter
.
The job of the Filter is to load out the secret key from the user secrets that we’ve previously specified in the user secrets file and compares it to the value from the querystring.
Items of note for the security key code:
- Code value has to be greater than 32 characters long
- Code value has to be less than 128 characters long
However, if you implement your own WebHookVerifyCodeFilter
you can override the EnsureValidCode
method as it is virtual and use what ever you method you like to look up your secret code. In a similar way, the GetSecretKey
method is also virtual so could write your own implementation and ignore the min/max requirements.
Side note: Getting the security keys configuration section is also custom so the structure we’ve specified at the top of this post can be custom. This is achieved through overriding the
GetSecretKeys
method.
Once you have the code and the secret code it compares the values to make sure it is the same. This looks pretty optimal so I would recommend re-using this if you are planning on writing your own code verifier and want to check 2 values.
After running through the rest of the filters in the pipeline then you should be able to hit a break point in your Controller action and have the id, event name and data object model bound and ready to interrogate and process.
Incorrect code mapping
As we’ve seen the AzureAlertMetadata
is defined due to the marker interface IWebHookVerifyCodeMetadata
to require a code query string parameter. So what happens if the code is incorrect or not provided at all?
As part of the processing filter pipeline there are various points of validation code and as part of the code verification filter it checks to make sure the values exist and are valid.
If the code is incorrect or not provided then you want to the system to stop processing and log an error.
If an incorrect value is submitted:
The ‘code’ query parameter provided in the HTTP request did not match the expected value.
If no value is present at all:
A ‘azurealert’ WebHook request must contain a ‘code’ query parameter.
These responses are returned by BadRequestObjectResult IActionResult instances which return a http status 400 Bad Request. These values are returned by the EnsureValidCode
method in the WebHookVerifyCodeFilter
in the OnResourceExecuting
method.
Once a BadRequestObjectResult instance is returned in a Filter through the ResourceExecutingContext.Result
property the filter pipeline stops processing. This happens at any point of the pipeline if a IActionResult is assigned to the Result property of the context. But why?
The documentation comments in the source code for the ResourceExecutingContext
in the MVC filters sums it up quite well.
/// Setting <see cref="Result"/> to a non-<c>null</c> value inside a resource filter will
/// short-circuit execution of additional resource filters and the action itself.
/// </remarks>
Well that explains that!
Conclusion
In this post we’ve looked at the security secrets values, how they map to the webhook route and how they are checked.
Any questions/comments then please contact me on Twitter @WestDiscGolf