Last active
January 20, 2025 15:40
-
-
Save davepcallan/807bdc3c51f4cf8912631549701d6483 to your computer and use it in GitHub Desktop.
Example Azure Function code for handling a Github webhook for a repo push event
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Security.Cryptography; | |
using System.Text; | |
using System.Text.Json; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.Azure.Functions.Worker; | |
using Microsoft.Extensions.Logging; | |
namespace AZFunctionsGHWebhooksDemo; | |
public class HandleRepoPush(ILogger<HandleRepoPush> logger) | |
{ | |
private const string GitHubSignatureHeader = "X-Hub-Signature-256"; | |
private const string GitHubWebhookSecret = "123456"; // This comes from the vault etc. in a real app | |
[Function(nameof(HandleRepoPush))] | |
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req) | |
{ | |
// Step 1: Get the GitHub Signature Header | |
if (!req.Headers.TryGetValue(GitHubSignatureHeader, out var signatureHeader)) | |
{ | |
logger.LogWarning($"Missing {GitHubSignatureHeader} header."); | |
return new UnauthorizedResult(); | |
} | |
// Step 2: Read the request body | |
string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); | |
// Step 3: Compute the HMAC hash | |
string computedHash = ComputeHash(requestBody, GitHubWebhookSecret); | |
// Step 4: Compare the hashes | |
if (!CheckHashes(signatureHeader, computedHash)) | |
{ | |
logger.LogWarning("Invalid HMAC signature."); | |
return new UnauthorizedResult(); | |
} | |
GitHubPushPayload? payload; | |
try | |
{ | |
payload = JsonSerializer.Deserialize<GitHubPushPayload>(requestBody); | |
} | |
catch (JsonException ex) | |
{ | |
logger.LogError(ex, "Failed to deserialize payload."); | |
return new BadRequestObjectResult("Invalid JSON payload."); | |
} | |
logger.LogInformation(new string('-', 50)); | |
logger.LogInformation($"Received push event for repository: {payload.repository.full_name}"); | |
foreach (var commit in payload.commits) | |
{ | |
logger.LogInformation($"Commit ID: {commit.id}"); | |
logger.LogInformation($"Commit Message: {commit.message}"); | |
logger.LogInformation($"Commit URL: {commit.url}"); | |
logger.LogInformation($"Commit Timestamp: {commit.timestamp}"); | |
logger.LogInformation($"Committer Username: {commit.author.username}"); | |
logger.LogInformation($"Modified Files: {string.Join(", ", commit.modified)}"); | |
} | |
return new OkObjectResult("Webhook received and processed."); | |
} | |
private static string ComputeHash(string payload, string secret) | |
{ | |
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); | |
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); | |
return "sha256=" + BitConverter.ToString(hash).Replace("-", "").ToLower(); | |
} | |
private static bool CheckHashes(string signatureHeader, string computedHash) | |
{ | |
// Compare hashes in a time-safe manner to prevent timing attacks | |
return CryptographicOperations.FixedTimeEquals( | |
Encoding.UTF8.GetBytes(signatureHeader), | |
Encoding.UTF8.GetBytes(computedHash) | |
); | |
} | |
} | |
public record GitHubPushPayload(Repository repository, Commit[] commits); | |
public record Repository(string full_name); | |
public record Commit( | |
string id, | |
string message, | |
string url, | |
DateTime timestamp, | |
CommitAuthor author, | |
string[] modified | |
); | |
public record CommitAuthor(string username); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment