WebHooks with Azure Event Grid and CloudEvents v1.0

CloudEvents v1.0 is finally here and it has been exciting to see it’s rapid growth and adoption. The milestone is the culmination of years of collaboration between many thought-leaders, cloud providers and members of a vibrant, open-source community.

Azure Event Grid has always provided first-class support for CloudEvents. And now, Event Grid supports the v1.0 specification with plans to adopt CloudEvents as the default schema when publishing events.

grid-cloudevents

This post will demonstrate how to subscribe to Event Grid events that use the CloudEvents v1.0 schema. The technologies used to handle the events will include Azure API Management, Azure Functions and ASP.NET Core Web APIs.

About CloudEvents

CloudEvents is an open-source specification that aims to provide an agreed upon, consistent approach for the declaration of an event. This image highlights it’s use as a format that can be leveraged across all different types of devices, systems and services:

cloudevents

CloudEvents v1.0 example

The complete details for the v1.0 specification and schema are available at https://github.com/cloudevents/spec/blob/v1.0/spec.md.

Here is an example of an event in the JSON format (gist link):

{
"specversion" : "1.0",
"id" : "b85d631a-101e-005a-02f2-cee7aa06f148",
"type" : "zohan.music.request",
"source" : "https://zohan.dev/music/",
"subject" : "zohan/music/requests/4322",
"time" : "2020-09-14T10:00:00Z",
"data" : {
"artist": "Gerardo",
"song": "Rico Suave"
}
}

A simple and extendable envelope is used to identify key artifacts about the message. This includes the specification version, an event identifier and other useful fields. The data property is designated for the actual payload that is relevant to the event. For example, this may include information about a blob from a Azure storage account, telemetry details from an IoT device, or in this case, a custom event that contains details about a song request.

Momentum and adoption

In addition to Event Grid, a growing list of the key participants, that now support CloudEvents, include: Red Hat’s EventFlow, Knative Eventing, Debezium, SAP’s Kyma project, Serverless.com Event Gateway and many more.

If you are interested in learning more about it’s purpose and the v1.0 release, I encourage you to visit the cloudevents.io site. Also, if you are new to Azure Event Grid, take a moment to visit the documentation, here.

Webhooks for event delivery

I’m going to focus on webhooks as a delivery target, since that is what will be used for the handlers in this article. But, before diving into the implementation details, it’s important to understand how validating a webhook occurs for CloudEvents v1.0

The HTTP OPTIONS method

The OPTIONS method is commonly used to request information about the communication choices that are available from a target, such as a webhook. In the case of CloudEvents, it serves two purposes:

  1. To protect the sender (Event Grid, in this example) from pushing messages to an endpoint that has not agreed to receiving notifications.
  2. To allow the receiver (the webhook) to indicate if it approves of the ability to have notifications delivered to it.

For both the sender and receiver, the primary driver is abuse protection. The most common issue this addresses is denial-of-service (DDOS) attacks.

Validating HTTP webhooks

When Event Grid attempts to create an event subscription, it makes a request to the target using the HTTP OPTIONS method. The primary intent of the request is to ask for permission to send notifications.

It’s important to note that this simple handshake does not replace any forms of authentication or authorization.

Validation request

The validation request contains some of the following important header fields:

  • WebHook-Request-Origin. This field identifies the sender and any other systems that act on it’s behalf.
  • WebHook-Request-Callback. An optional field that provides the webhook with an alternative to grant permission asynchronously, by way of a HTTP callback.
  • WebHook-Request-Rate. The maximum number of requests per minute that the sender will be sending.

Validation response

The target has two options for allowing the delivery of events:

  1. Reply to the validation request by including the WebHook-Allowed-Origin and WebHook-Allowed-Rate headers fields and their corresponding values.
  2. Reply to the validation request without the response headers and grant permission by executing a call to the callback URL.

All of the examples in this post will demonstrate the first option. The second option should only be used if there isn’t a mechanism available to support the necessary response. Some examples that come to mind could be third-party workflow solutions that do not allow users the ability to manipulate header fields.

The CloudEvents specification covers this portion in more depth, here.

ASP.NET Core Web API example

An example of a controller that handles the OPTIONS method in ASP.NET Core could look similar to the following code snippet (gist link):

[HttpOptions]
public async Task<IActionResult> Options()
{
using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
{
// Retrieve the validation header fields
var webhookRequestOrigin = HttpContext.Request.Headers["WebHook-Request-Origin"].FirstOrDefault();
var webhookRequestCallback = HttpContext.Request.Headers["WebHook-Request-Callback"];
var webhookRequestRate = HttpContext.Request.Headers["WebHook-Request-Rate"];
// Respond with the appropriate origin and allowed rate to
// confirm acceptance of incoming notications
HttpContext.Response.Headers.Add("WebHook-Allowed-Rate", "*");
HttpContext.Response.Headers.Add("WebHook-Allowed-Origin", webhookRequestOrigin);
}
return Ok();
}

This exact approach is used in the Event Grid Viewer application (https://aka.ms/eventgridviewer) that you can deploy today to view and test your events. A walk through of how to set it up is provided, here. This application now supports the v1.0 schema and validation process.

If we send the same payload as the v1.0 example that was given earlier to an Event Grid Topic, the viewer will display it accordingly:

cloudevent-viewer

Azure Functions example

The existing Event Grid Trigger for an Azure Function does not support the v1.0 schema yet. If you wish to have a Azure Function be a handler, then it must be a HTTP triggered function for now.

To support the validation step, we first need to add options to the supported methods in the function. Then, take a similiar approach to the previous example by setting the origin and rate header fields in the response (gist link):

public static class TestFuncApi
{
[FunctionName("TestFuncApi")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", "options", Route = null)] HttpRequest req,
ILogger log)
{
if (req.Method == "OPTIONS")
{
// Retrieve the request origin
if (!req.Headers.TryGetValue("WebHook-Request-Origin", out var headerValues))
return new BadRequestObjectResult("Not a valid request");
// Respond with the origin and rate
var webhookRequestOrigin = headerValues.FirstOrDefault();
req.HttpContext.Response.Headers.Add("WebHook-Allowed-Rate", "*");
req.HttpContext.Response.Headers.Add("WebHook-Allowed-Origin", webhookRequestOrigin);
return new OkResult();
}
// Process event notifications
// Do something here….
return new OkObjectResult("OK");
}
}

API Management example

API Management (APIM) and Event Grid integration has always been interesting to me. Luckily, registering a APIM endpoint for CloudEvents only involves a few steps.

It’s important to remember that the same URL is used for both the delivery of event notifications and the validation. In API Management, this means that we just need to create an operation for each of the methods, that use the same URL:

cloudevents-apim-setup

To support validation, the inbound processing step for the OPTIONS method looks like this (gist link):

<inbound>
<base />
<!– Get the WebHook-Request-Origin –>
<set-variable value="@(context.Request.Headers.GetValueOrDefault("WebHook-Request-Origin"))"
name="webhookRequestOrigin" />
<!–
Return the response with the allowed origin
and allowed rate to confirm the subscription.
–>
<return-response>
<set-status code="200" reason="OK" />
<set-header name="WebHook-Allowed-Origin" exists-action="override">
<value>@((string)context.Variables["webhookRequestOrigin"])</value>
</set-header>
<set-header name="WebHook-Allowed-Rate" exists-action="override">
<value>*</value>
</set-header>
<set-body />
</return-response>
</inbound>

A key takeaway

One of the main takeaways for me when learning about CloudEvents was understanding that the format for an event isn’t just isolated to a particular project or solution. It can be used, and is really showcased, when it is employed consistently throughout the entire life cycle – both internally and across all boundaries.

An event can originate from an IoT device that is running on-premises, make it’s way to a public cloud provider and traverse across multiple systems, while still maintaining a uniformed format. The notion that the definition of an event is not coupled to a cloud provider, vendor or custom solution speaks volumes about the importance and direction of recognizing an event as a first-class object in our designs.

References

Special shout-out to Clemens Vasters for his contributions to the CloudEvents project.

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s