using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace DiunaBI.API.Attributes; /// /// Authorization attribute that validates API key from X-API-Key header. /// Uses constant-time comparison to prevent timing attacks. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class ApiKeyAuthAttribute : Attribute, IAuthorizationFilter { private const string ApiKeyHeaderName = "X-API-Key"; public void OnAuthorization(AuthorizationFilterContext context) { var configuration = context.HttpContext.RequestServices.GetRequiredService(); var logger = context.HttpContext.RequestServices.GetRequiredService>(); // Get expected API key from configuration var expectedApiKey = configuration["apiKey"]; if (string.IsNullOrEmpty(expectedApiKey)) { logger.LogError("API key not configured in appsettings"); context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); return; } // Get API key from header if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey)) { logger.LogWarning("API key missing from request header"); context.Result = new UnauthorizedObjectResult(new { error = "API key is required" }); return; } // Constant-time comparison to prevent timing attacks if (!IsApiKeyValid(extractedApiKey!, expectedApiKey)) { logger.LogWarning("Invalid API key provided from {RemoteIp}", context.HttpContext.Connection.RemoteIpAddress); context.Result = new UnauthorizedObjectResult(new { error = "Invalid API key" }); return; } // API key is valid - allow the request to proceed } /// /// Constant-time string comparison to prevent timing attacks. /// private static bool IsApiKeyValid(string providedKey, string expectedKey) { if (providedKey == null || expectedKey == null) return false; var providedBytes = Encoding.UTF8.GetBytes(providedKey); var expectedBytes = Encoding.UTF8.GetBytes(expectedKey); return CryptographicOperations.FixedTimeEquals(providedBytes, expectedBytes); } }