Friday, 27 July 2018

Hosting a C# Webhook Listener in Azure

In previous blog entries we’ve been introduced to webhooks in Alma, and we’ve learned how to build and host a webhook listener in the public cloud using the AWS API Gateway and Lambda service. In this article, we will build a webhook listener in C# and host it on the Microsoft Azure cloud service.
As in previous examples, our listener will need to perform the following:
  • Answer a challenge from Alma
  • Validate the signature of incoming requests
  • Process events according to the event type
In this example, we will be processing webhooks that are fired by Alma when a job ends. If the job is of type export, we download the resulting file from the FTP server, extract the contents, and create a new file on Dropbox for each exported BIB. This workflow simulates a typical business scenario of additional processing that must happen outside of Alma for records meeting certain criteria.

Getting Started

A webhook listener is simply a REST endpoint which accepts requests at a particular URL. To build a REST endpoint in .NET, we can use the ASP.NET Web API project type. Using Visual Studio 2015 (any edition), click File -> New Project and select ASP.NET Web Application.
Azure Webhooks - New Project
Click OK, and then select Web API in the next window.
Azure Webhooks - Web API
Now add a WebhooksController.cs file to the Controllers folder. We’re ready to begin writing our listener code.

Answer Challenge

When registering a new webhook integration profile, Alma needs to verify that there is a valid listener at that URL which expects to receive requests. To do so, Alma sends a GET request with a challenge parameter. It expects our listener to respond with the provided challenge. So we create a new method in our controller and specify HttpGet as the method attribute. The method creates a simple dynamic object and returns the object to the calling application:
[HttpGet]
public IHttpActionResult Challenge(string challenge)
{
   dynamic response = new ExpandoObject();
   response.challenge = challenge;
   return Ok(response);
}

Process requests

Alma sends webhook events as POST requests to our listener. So we create a new method in our controller and specify HttpPost as the method attribute. The method accepts a single parameter- a parsed JSON object (JObject). We tell ASP.NET to populate the parameter using the request body with the FromBody attribute. 
[HttpPost]
public async Task<IHttpActionResult> ProcessWebhook([FromBody]JObject body)
Now we’re ready to validate the signature and process the event.

Validate Signature

We extract the signature value from the X-Exl-Signature header. Then we perform a SHA256-based message hash on the body using the shared secret configured in Alma. We compare the value to the one received in the header. If the value doesn’t match, we return an unauthorized error code, fearing the message has been tampered with.
string signature = 
   Request.Headers.GetValues("X-Exl-Signature").First();
if (!ValidateSignature(
      body.ToString(Newtonsoft.Json.Formatting.None),
      signature,
      ConfigurationManager.AppSettings["WebhookSecret"])
   )
{
   return Unauthorized();
}
The ValidateSignature method creates a SHA256 hash object initialized with the shared secret, uses the object to hash the message body, and then converts the hash to base64.
private bool ValidateSignature(string body, 
   string signature, string secret)
{
   var hash = new System.Security.Cryptography.HMACSHA256(
      Encoding.ASCII.GetBytes(secret));
   var computedSignature = Convert.ToBase64String(
      hash.ComputeHash(Encoding.ASCII.GetBytes(body)));
   return computedSignature == signature;
}

Processing the Event

We now retrieve the webhook action from the body. Based on the action type, we call a method which processes those types of events. The method is available on a WebHookHandler model.
string action = body["action"].ToString();
switch (action.ToLower())
{
   case "job_end":
      WebhookHandler handler = new WebhookHandler();
      await handler.JobEnd(body["job_instance"]);
      return Ok();
   default:
      return BadRequest();
}

Performing the application logic

In our simulation of real business logic, we will be processing bibliographic export jobs. We will download the exported file from the FTP server, extract the bibliographic records, transform each record using XSLT, and upload a new file per record to our Dropbox account.
We use Newtonsoft’s JSON Linq syntax to extract the filename from the job instance. GThe job instance includes a number of counter values. We’re looking for the one which has a type of “c.jobs.bibExport.link”. The Linq syntax to find that counter is as follows:
jobInstance.SelectToken("$.counter[?(@.type.value=='c.jobs.bibExport.link')]");
Assuming the job instance includes an export file, we download the file from the configured FTP site. We then parse the XML file and extract each record. For each record, we perform an XSLT transformation, and then use the Dropbox API to upload the resulting files to our Dropbox account. 

Deploying to Azure

We can test our listener locally by posting messages to the webservice from any REST client (such as Postman or the Advanced REST Client). Once the service is working correctly, we want to publish it to Azure. We log in to the Azure portal and select New, then choose Web App. After providing a unique name and accepting the defaults for the other settings, Azure deploys our new, empty web application. 
Azure Webhooks - Overview

Application Settings

Some of our application settings are stored in the web.config file. These include the host and directory for the FTP site from which we download the exported file. However, there are other settings which should not be stored in source control, including the FTP username and password and the Dropbox token. We store those in a separate file that we’ve called web.secrets.config. We add a reference to that file from our web.config:
<appSettings file="Web.secrets.config">
   <add key="FtpHost" value="ftp.exlibris-usa.com" />
   <add key="FtpDir" value="public/TR_INTEGRATION_INST/export/" />
</appSettings>
Since that file doesn’t get deployed to our web app, we need to set those values in another way. Azure provides the ability to set application settings in the portal. The settings configured there override any values in the various configuration files.
Azure Webhooks - Settings

 

Publish from Visual Studio

You can use Git to publish an application to Azure, or you can use the tooling provided within Visual Studio. Under the Project menu, select the Publish to Azure option. Log in with a valid Azure account, and select the web app we created in the previous section. The application is deployed and is available at the custom URL. We test the application by performing a GET request with a challenge parameter and validating that our service has echoed back the challenge.

Putting it together

Now that our application is deployed, we are ready to configure Alma. Create a new Webhook integration profile and provide the URL of our service hosted on Azure. Specify a secret for the signature, being sure to use the same secret configured in our application settings. Specify JSON as the format, and save the integration profile.
Whenever a job finishes, Alma will call out to our webhook on Azure. We can test this by running a job within Alma and watching the real time log stream in the Azure portal. We see the challenge being logged when we register the webhook integration profile, and we see the job event coming in when the job we ran completes.
Azure Webhooks - Log

Leveraging the public cloud is a cost effective way to deploy services which extend Alma’s core functionality. The cloud allows us to focus on the desired functionality without the need for locally-hosted infrastructure. Given the choices in the market today, there are good options for any development stack. 
All of the code for this example is available in this Github repository

No comments:

Post a Comment