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 { /// Validates X-Ingest-Key for POST /api/workorders/ingest. public class IngestApiKeyFilter : IAsyncActionFilter { private readonly WorkOrderIngestOptions _options; public IngestApiKeyFilter(IOptions 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)) { } } }