Adam Storr - ASP.NET Core WebHooks - Secret Values

ASP.NET Core WebHooks - Secret Values

Looking at the secret values and how they work to allow for the AzureAlertWebHook webhook to process.

Published on 16 February 2018

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

  1. Webhooks

    This is the high level configuration property for all the web hooks. This constant value is defined in the WebHookConstants.ReceiverConfigurationSectionKey property.

  2. azurealert

    This is what is known as the Receiver Name. It gets registered through the AzureAlertMetadata constructor specifying it to the base WebHookMetadata class. This metadata class is registered at start up using the AddAzureAlertWebHooks() middleware method.

  3. SecretKey

    This defines the secret key section. The constant value is defined in the WebHookConstants.SecretKeyConfigurationKeySectionKey property.

  4. 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:

  1. Code value has to be greater than 32 characters long
  2. 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