Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scheduler.WebService.Options;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.EventWebhooks;
|
||||
|
||||
public interface IWebhookRequestAuthenticator
|
||||
{
|
||||
Task<WebhookAuthenticationResult> AuthenticateAsync(HttpContext context, ReadOnlyMemory<byte> body, SchedulerWebhookOptions options, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed class WebhookRequestAuthenticator : IWebhookRequestAuthenticator
|
||||
{
|
||||
private readonly ILogger<WebhookRequestAuthenticator> _logger;
|
||||
|
||||
public WebhookRequestAuthenticator(
|
||||
ILogger<WebhookRequestAuthenticator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<WebhookAuthenticationResult> AuthenticateAsync(HttpContext context, ReadOnlyMemory<byte> body, SchedulerWebhookOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return WebhookAuthenticationResult.Success();
|
||||
}
|
||||
|
||||
if (options.RequireClientCertificate)
|
||||
{
|
||||
var certificate = context.Connection.ClientCertificate ?? await context.Connection.GetClientCertificateAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (certificate is null)
|
||||
{
|
||||
_logger.LogWarning("Webhook {Name} rejected request without client certificate.", options.Name);
|
||||
return WebhookAuthenticationResult.Fail(StatusCodes.Status401Unauthorized, "Client certificate required.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.HmacSecret))
|
||||
{
|
||||
var headerName = string.IsNullOrWhiteSpace(options.SignatureHeader) ? "X-Scheduler-Signature" : options.SignatureHeader;
|
||||
if (!context.Request.Headers.TryGetValue(headerName, out var signatureValues))
|
||||
{
|
||||
_logger.LogWarning("Webhook {Name} rejected request missing signature header {Header}.", options.Name, headerName);
|
||||
return WebhookAuthenticationResult.Fail(StatusCodes.Status401Unauthorized, "Missing signature header.");
|
||||
}
|
||||
|
||||
var providedSignature = signatureValues.ToString();
|
||||
if (string.IsNullOrWhiteSpace(providedSignature))
|
||||
{
|
||||
return WebhookAuthenticationResult.Fail(StatusCodes.Status401Unauthorized, "Signature header is empty.");
|
||||
}
|
||||
|
||||
if (!VerifySignature(body.Span, options.HmacSecret!, providedSignature))
|
||||
{
|
||||
_logger.LogWarning("Webhook {Name} rejected request with invalid signature.", options.Name);
|
||||
return WebhookAuthenticationResult.Fail(StatusCodes.Status401Unauthorized, "Invalid signature.");
|
||||
}
|
||||
}
|
||||
|
||||
return WebhookAuthenticationResult.Success();
|
||||
}
|
||||
|
||||
private static bool VerifySignature(ReadOnlySpan<byte> payload, string secret, string providedSignature)
|
||||
{
|
||||
byte[] secretBytes;
|
||||
try
|
||||
{
|
||||
secretBytes = Convert.FromBase64String(secret);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
try
|
||||
{
|
||||
secretBytes = Convert.FromHexString(secret);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
}
|
||||
}
|
||||
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var hash = hmac.ComputeHash(payload.ToArray());
|
||||
var computedSignature = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(computedSignature),
|
||||
Encoding.UTF8.GetBytes(providedSignature.Trim()));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct WebhookAuthenticationResult(bool Succeeded, int StatusCode, string? Message)
|
||||
{
|
||||
public static WebhookAuthenticationResult Success() => new(true, StatusCodes.Status200OK, null);
|
||||
|
||||
public static WebhookAuthenticationResult Fail(int statusCode, string message) => new(false, statusCode, message);
|
||||
|
||||
public IResult ToResult()
|
||||
=> Succeeded ? Results.Ok() : Results.Json(new { error = Message ?? "Unauthorized" }, statusCode: StatusCode);
|
||||
}
|
||||
Reference in New Issue
Block a user