Skip to content

Commit a974955

Browse files
authored
Merge pull request #44 from rubberduck-vba/webhook
Fix webhook signature validation
2 parents 09d7281 + 1255e09 commit a974955

3 files changed

+128
-79
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11

22
using Microsoft.AspNetCore.Authentication;
33
using Microsoft.Extensions.Options;
4-
using rubberduckvba.Server.Api.Admin;
54
using rubberduckvba.Server.Services;
65
using System.Security.Claims;
7-
using System.Security.Cryptography;
8-
using System.Text;
96
using System.Text.Encodings.Web;
107

118
namespace rubberduckvba.Server;
@@ -35,79 +32,3 @@ protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
3532
: AuthenticateResult.NoResult();
3633
}
3734
}
38-
39-
public class WebhookAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
40-
{
41-
private readonly ConfigurationOptions _configuration;
42-
43-
public WebhookAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder,
44-
ConfigurationOptions configuration)
45-
: base(options, logger, encoder)
46-
{
47-
_configuration = configuration;
48-
}
49-
50-
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
51-
{
52-
return await Task.Run(() =>
53-
{
54-
var xGitHubEvent = Context.Request.Headers["X-GitHub-Event"];
55-
var xGitHubDelivery = Context.Request.Headers["X-GitHub-Delivery"];
56-
var xHubSignature = Context.Request.Headers["X-Hub-Signature"];
57-
var xHubSignature256 = Context.Request.Headers["X-Hub-Signature-256"];
58-
59-
if (!xGitHubEvent.Contains("push"))
60-
{
61-
// only authenticate push events
62-
return AuthenticateResult.NoResult();
63-
}
64-
65-
if (!Guid.TryParse(xGitHubDelivery.SingleOrDefault(), out _))
66-
{
67-
// delivery should parse as a GUID
68-
return AuthenticateResult.NoResult();
69-
}
70-
71-
if (!xHubSignature.Any())
72-
{
73-
// signature header should be present
74-
return AuthenticateResult.NoResult();
75-
}
76-
77-
var signature = xHubSignature256.SingleOrDefault();
78-
79-
using var reader = new StreamReader(Context.Request.Body);
80-
var payload = reader.ReadToEndAsync().GetAwaiter().GetResult();
81-
82-
if (!IsValidSignature(signature, payload))
83-
{
84-
// encrypted signature must be present
85-
return AuthenticateResult.NoResult();
86-
}
87-
88-
var identity = new ClaimsIdentity("webhook", ClaimTypes.Name, ClaimTypes.Role);
89-
identity.AddClaim(new Claim(ClaimTypes.Name, "rubberduck-vba-releasebot"));
90-
identity.AddClaim(new Claim(ClaimTypes.Role, "rubberduck-webhook"));
91-
identity.AddClaim(new Claim(ClaimTypes.Authentication, "webhook-signature"));
92-
93-
var principal = new ClaimsPrincipal(identity);
94-
return AuthenticateResult.Success(new AuthenticationTicket(principal, "webhook-signature"));
95-
});
96-
}
97-
98-
private bool IsValidSignature(string? signature, string payload)
99-
{
100-
if (string.IsNullOrWhiteSpace(signature))
101-
{
102-
return false;
103-
}
104-
105-
using var sha256 = SHA256.Create();
106-
107-
var secret = _configuration.GitHubOptions.Value.WebhookToken;
108-
var bytes = Encoding.UTF8.GetBytes(secret + payload);
109-
var check = $"sha256={Encoding.UTF8.GetString(sha256.ComputeHash(bytes))}";
110-
111-
return check == payload;
112-
}
113-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+

2+
using Microsoft.AspNetCore.Authentication;
3+
using Microsoft.Extensions.Options;
4+
using System.Security.Claims;
5+
using System.Text.Encodings.Web;
6+
7+
namespace rubberduckvba.Server;
8+
9+
public class WebhookAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
10+
{
11+
private readonly WebhookSignatureValidationService _service;
12+
13+
public WebhookAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder,
14+
WebhookSignatureValidationService service)
15+
: base(options, logger, encoder)
16+
{
17+
_service = service;
18+
}
19+
20+
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
21+
{
22+
return await Task.Run(() =>
23+
{
24+
var userAgent = Context.Request.Headers.UserAgent;
25+
var xGitHubEvent = Context.Request.Headers["X-GitHub-Event"].OfType<string>().ToArray();
26+
var xGitHubDelivery = Context.Request.Headers["X-GitHub-Delivery"].OfType<string>().ToArray();
27+
var xHubSignature = Context.Request.Headers["X-Hub-Signature"].OfType<string>().ToArray();
28+
var xHubSignature256 = Context.Request.Headers["X-Hub-Signature-256"].OfType<string>().ToArray();
29+
30+
using var reader = new StreamReader(Context.Request.Body);
31+
var payload = reader.ReadToEndAsync().GetAwaiter().GetResult();
32+
33+
if (_service.Validate(payload, userAgent, xGitHubEvent, xGitHubDelivery, xHubSignature, xHubSignature256))
34+
{
35+
var principal = CreatePrincipal();
36+
var ticket = new AuthenticationTicket(principal, "webhook-signature");
37+
38+
return AuthenticateResult.Success(ticket);
39+
}
40+
41+
return AuthenticateResult.NoResult();
42+
});
43+
}
44+
45+
private static ClaimsPrincipal CreatePrincipal()
46+
{
47+
var identity = new ClaimsIdentity("webhook", ClaimTypes.Name, ClaimTypes.Role);
48+
49+
identity.AddClaim(new Claim(ClaimTypes.Name, "rubberduck-vba-releasebot"));
50+
identity.AddClaim(new Claim(ClaimTypes.Role, "rubberduck-webhook"));
51+
identity.AddClaim(new Claim(ClaimTypes.Authentication, "webhook-signature"));
52+
53+
var principal = new ClaimsPrincipal(identity);
54+
return principal;
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using rubberduckvba.Server.Api.Admin;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
5+
namespace rubberduckvba.Server;
6+
7+
public class WebhookSignatureValidationService(ConfigurationOptions configuration)
8+
{
9+
public bool Validate(
10+
string payload,
11+
string? userAgent,
12+
string[] xGitHubEvent,
13+
string[] xGitHubDelivery,
14+
string[] xHubSignature,
15+
string[] xHubSignature256
16+
)
17+
{
18+
if (!(userAgent ?? string.Empty).StartsWith("GitHub-Hookshot/"))
19+
{
20+
// user agent must be GitHub hookshot
21+
return false;
22+
}
23+
24+
if (!xGitHubEvent.Contains("push"))
25+
{
26+
// only authenticate push events
27+
return false;
28+
}
29+
30+
if (!Guid.TryParse(xGitHubDelivery.SingleOrDefault(), out _))
31+
{
32+
// delivery should parse as a GUID
33+
return false;
34+
}
35+
36+
if (!xHubSignature.Any())
37+
{
38+
// SHA-1 signature header must be present
39+
return false;
40+
}
41+
42+
var signature = xHubSignature256.SingleOrDefault();
43+
if (signature == default)
44+
{
45+
// SHA-256 signature header must be present
46+
return false;
47+
}
48+
49+
if (!IsValidSignature(signature, payload))
50+
{
51+
// SHA-256 signature must match
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
58+
private bool IsValidSignature(string? signature, string payload)
59+
{
60+
if (string.IsNullOrWhiteSpace(signature))
61+
{
62+
return false;
63+
}
64+
using var sha256 = SHA256.Create();
65+
66+
var secret = configuration.GitHubOptions.Value.WebhookToken;
67+
var bytes = Encoding.UTF8.GetBytes(secret + payload);
68+
var check = $"sha256={Encoding.UTF8.GetString(sha256.ComputeHash(bytes))}";
69+
70+
return signature == check;
71+
}
72+
}

0 commit comments

Comments
 (0)