shoc-backend/Api.SeaHavenIndustries/Filters/IngestApiKeyFilter.cs

67 lines
2.4 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Api.SeaHavenIndustries.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
namespace Api.SeaHavenIndustries.Filters
{
/// <summary>Validates X-Ingest-Key for POST /api/workorders/ingest.</summary>
public class IngestApiKeyFilter : IAsyncActionFilter
{
private readonly WorkOrderIngestOptions _options;
public IngestApiKeyFilter(IOptions<WorkOrderIngestOptions> options)
{
_options = options.Value;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!_options.Enabled)
{
context.Result = new ObjectResult(new { message = "Work order ingest is disabled." })
{
StatusCode = StatusCodes.Status503ServiceUnavailable
};
return;
}
if (string.IsNullOrWhiteSpace(_options.ApiKey)
|| _options.ApiKey.Contains("${", StringComparison.Ordinal))
{
context.Result = new ObjectResult(new { message = "Ingest API key is not configured." })
{
StatusCode = StatusCodes.Status503ServiceUnavailable
};
return;
}
if (!context.HttpContext.Request.Headers.TryGetValue("X-Ingest-Key", out var provided)
|| !FixedTimeEquals(provided.ToString(), _options.ApiKey))
{
context.Result = new UnauthorizedObjectResult(new { message = "Invalid or missing X-Ingest-Key." });
return;
}
await next();
}
private static bool FixedTimeEquals(string provided, string expected)
{
var providedBytes = Encoding.UTF8.GetBytes(provided);
var expectedBytes = Encoding.UTF8.GetBytes(expected);
return providedBytes.Length == expectedBytes.Length
&& CryptographicOperations.FixedTimeEquals(providedBytes, expectedBytes);
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class IngestApiKeyAttribute : ServiceFilterAttribute
{
public IngestApiKeyAttribute() : base(typeof(IngestApiKeyFilter))
{
}
}
}