
Consuming Webhooks with Logic Apps
Scenario
The scenario covers a situation where you manage a public GitHub repo, but your real development happens in Visual Studio Team Services. In such a case, you want every GitHub issue that gets created to be automatically placed on the VSTS backlog. Let’s have a look!
Logic Apps has an extremely handy webhook trigger. The trigger can be configured, so it registers the webhook when the Logic App gets enabled and that deregistration occurs on a Logic App disable. These are the necessary steps to setup this devops integration scenario.
Register and deregister the webhook
- Create a Logic App with the webhook trigger.
- Configure the trigger properties to register the webhook:
- Subscribe Method: POST
- Subscribe URI: https://api.github.com/repos/{userName}/{repoName}/hooks
- Subscribe Body: request according to the documentation, subscribing solely on the issues event. As the url, you need to pass this expression: @{listCallbackUrl()}
- Configure the trigger properties to un-register the webhook:
- Unsubscribe Method: DELETE
- Unsubscribe URI: compose the URI with the returned GitHub webhook id, via this expression:
@{concat(‘https://api.github.com/repos/{userName}/{repoName}/hooks/’, triggerOutputs().subscribe.body.id)}
- Both operations must use Basic Authentication to authenticate against the GitHub API. The password should be retrieved from Key Vault, at deploy time.
Validate hash-based message authentication code
It’s advised to validate the HMAC code, that is provided in the X-Hub-Signature header, as explained over here. In order to achieve this, we’ll introduce an Azure Function. The basis for this function is taken from this blog.
- Create an Azure Function “ValidateRequest” with this logic:
#r “System.Security” |
using System.Net; |
using System.Text; |
using System.Security.Cryptography; |
private const string Sha1Prefix = “sha1=“; |
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log) |
{ |
log.Info(“C# HTTP trigger function processed a request.“); |
string signatureWithPrefix = req.Headers.GetValues(“X-Hub-Signature“).FirstOrDefault(); |
log.Info(“Signature: “ + signatureWithPrefix); |
string sharedSecret = req.Headers.GetValues(“X-LogicApp-Secret“).FirstOrDefault(); |
log.Info(“Secret: “ + sharedSecret); |
var payload = await req.Content.ReadAsStringAsync(); |
log.Info(“Payload: “ + payload); |
bool isValid = IsGithubPushAllowed(payload, signatureWithPrefix, sharedSecret); |
return isValid == false |
? req.CreateResponse(HttpStatusCode.BadRequest, “Missing or invalid Github security headers!“) |
: req.CreateResponse(HttpStatusCode.OK); |
} |
public static bool IsGithubPushAllowed(string payload, string signatureWithPrefix, string sharedSecret) |
{ |
if (string.IsNullOrWhiteSpace(payload)) |
{ |
throw new ArgumentNullException(nameof(payload)); |
} |
if (string.IsNullOrWhiteSpace(signatureWithPrefix)) |
{ |
throw new ArgumentNullException(nameof(signatureWithPrefix)); |
} |
if (signatureWithPrefix.StartsWith(Sha1Prefix, StringComparison.OrdinalIgnoreCase)) |
{ |
var signature = signatureWithPrefix.Substring(Sha1Prefix.Length); |
var secret = Encoding.ASCII.GetBytes(sharedSecret); |
var payloadBytes = Encoding.ASCII.GetBytes(payload); |
using (var hmSha1 = new HMACSHA1(secret)) |
{ |
var hash = hmSha1.ComputeHash(payloadBytes); |
var hashString = ToHexString(hash); |
if (hashString.Equals(signature)) |
{ |
return true; |
} |
} |
} |
return false; |
} |
public static string ToHexString(byte[] bytes) |
{ |
var builder = new StringBuilder(bytes.Length * 2); |
foreach (byte b in bytes) |
{ |
builder.AppendFormat(“{0:x2}“, b); |
} |
return builder.ToString(); |
} |
- Call this Azure Function from within the Logic App:
- Pass the complete request body
- Pass the HTTP headers and add the “X-LogicApp-Secret” header via this expression:
@addProperty(triggerOutputs()[‘headers’], ‘X-LogicApp-Secret’, ‘ThisIsMySecret’)
Handle the GitHub webhook
- Use the Parse JSON action to be able to easily access the event data in the next actions. Use a sample request to generate the JSON schema.
- GitHub sends each 5 minutes a ping request. Implement some decision logic to cancel the workflow in case we receive such a ping request. This can be verified through the X-GitHub-Event HTTP header.
- Another decision we need is to only continue processing in the case it’s a new issue that was created. This can be determined through the action, which should be equal to opened.
Create the VSTS work item
- Configure the “Create a work item” action, including the title and description taken from the GitHub event.
Testing
- When we create an issue in GitHub:
- The Logic App gets immediately fired:
- And a VSTS work item is created:
Conclusion
Let’s see whether we fulfilled all responsibilities for webhook consumers:
- Availability: is native to serverless technology
- Scalability: is native to serverless technology, we can add throttling via concurrency control
- Reliability: Logic Apps automatically persists the message. Retry policies can be configured towards the backend system:
- Security: HMAC code has been verified through Azure Function
- Sequencing: not really needed, only new issues are taken into account
Logic Apps provides seamless and incredibly easy integration for webhooks!