From d07bc2418b973dc8fa2777b8f074f11b77c68826 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 20 Oct 2026 11:21:18 +0200 Subject: [PATCH] Add API key authentication middleware support Introduced `ApiKeyAuthOptions` for API key configuration, including a `Key` property and `SectionName` for binding. Updated `ApiKeyAuthMiddleware` to validate the `X-Api-Key` header against the configured API key. Added conditional middleware application in `Program.cs` based on the presence of a configured API key. Bound the `ApiKey` section in `appsettings.json` and `appsettings.Development.json` to store the API key, with a default value of `null`. This improves security and centralizes API key management. --- .../Middleware/ApiKeyAuthMiddleware.cs | 7 +++++-- .../Options/ApiKeyOptions.cs | 8 ++++++++ .../DigitalData.TaskFlow.API/Program.cs | 13 ++++++++++++- .../appsettings.Development.json | 3 +++ .../DigitalData.TaskFlow.API/appsettings.json | 3 +++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/presentation/DigitalData.TaskFlow.API/Options/ApiKeyOptions.cs diff --git a/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs b/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs index 7653f9a..5384d2f 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs @@ -1,6 +1,8 @@ using DigitalData.TaskFlow.API.Contracts.Common; +using DigitalData.TaskFlow.API.Options; using DigitalData.TaskFlow.Domain.Common; using DigitalData.TaskFlow.Domain.Enums; +using Microsoft.Extensions.Options; using System.Text.Json; namespace DigitalData.TaskFlow.API.Middleware; @@ -8,9 +10,10 @@ namespace DigitalData.TaskFlow.API.Middleware; /// /// Validates required API key header for API endpoints. /// -public sealed class ApiKeyAuthMiddleware(RequestDelegate next) +public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions apiKeyOptions) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + private readonly string? _configuredApiKey = apiKeyOptions.Value.Key; /// /// Executes middleware logic. @@ -25,7 +28,7 @@ public sealed class ApiKeyAuthMiddleware(RequestDelegate next) } if (context.Request.Headers.TryGetValue("X-Api-Key", out var values) - && !string.IsNullOrWhiteSpace(values.FirstOrDefault())) + && string.Equals(values.FirstOrDefault(), _configuredApiKey, StringComparison.Ordinal)) { await next(context); return; diff --git a/src/presentation/DigitalData.TaskFlow.API/Options/ApiKeyOptions.cs b/src/presentation/DigitalData.TaskFlow.API/Options/ApiKeyOptions.cs new file mode 100644 index 0000000..0a61256 --- /dev/null +++ b/src/presentation/DigitalData.TaskFlow.API/Options/ApiKeyOptions.cs @@ -0,0 +1,8 @@ +namespace DigitalData.TaskFlow.API.Options; + +public sealed class ApiKeyAuthOptions +{ + public const string SectionName = "ApiKey"; + + public string? Key { get; init; } +} diff --git a/src/presentation/DigitalData.TaskFlow.API/Program.cs b/src/presentation/DigitalData.TaskFlow.API/Program.cs index 6f4d115..eda400f 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Program.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Program.cs @@ -96,6 +96,10 @@ try .AddOptions() .Bind(config.GetSection(SwaggerOptions.SectionName)); + builder.Services + .AddOptions() + .Bind(config.GetSection(ApiKeyAuthOptions.SectionName)); + builder.Services.AddSerilogUi(options => { options.UseSqliteServer(databaseOptions => @@ -110,11 +114,18 @@ try var swaggerOptions = app.Services .GetRequiredService>() .Value; + var apiKeyOptions = app.Services + .GetRequiredService>() + .Value; var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction; + var enforceApiKey = !string.IsNullOrWhiteSpace(apiKeyOptions.Key); app.UseMiddleware(); - app.UseMiddleware(); + if (enforceApiKey) + { + app.UseMiddleware(); + } app.UseSerilogRequestLogging(); if (enableSwagger) diff --git a/src/presentation/DigitalData.TaskFlow.API/appsettings.Development.json b/src/presentation/DigitalData.TaskFlow.API/appsettings.Development.json index cf2df3e..951d6c5 100644 --- a/src/presentation/DigitalData.TaskFlow.API/appsettings.Development.json +++ b/src/presentation/DigitalData.TaskFlow.API/appsettings.Development.json @@ -10,6 +10,9 @@ } } }, + "ApiKey": { + "Key": null + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/presentation/DigitalData.TaskFlow.API/appsettings.json b/src/presentation/DigitalData.TaskFlow.API/appsettings.json index a269b53..eb0f98a 100644 --- a/src/presentation/DigitalData.TaskFlow.API/appsettings.json +++ b/src/presentation/DigitalData.TaskFlow.API/appsettings.json @@ -13,6 +13,9 @@ "ConnectionStrings": { "TaskFlowDb": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;" }, + "ApiKey": { + "Key": null + }, "MediatR": { "LuckyPennySoftLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxODE2MTI4MDAwIiwiaWF0IjoiMTc4NDYyNDU1NyIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiMDE5ODNjNTllNGIzNzI4ZWJmZDMxMDNjMmE0ODZlODUiLCJzdWJfaWQiOiItIiwiZWRpdGlvbiI6IjAiLCJ0eXBlIjoiMiJ9.IUUO926m9crYGYxMjjKD_n9BnUm-EDyjFIn0YmMUCo7C-QTwvB8WhXP8veTSFsBq-leIIDJ4jyl7Pgc_7ciwg1XhUSIs4mkQroEUaSFCGOxw7Pi41WM8MK5YFSaqLTYYXec9zxgiJbGzABbh3CHTSup3okGnVm_CMoPEs91l2c0A6N1JyZy74urd_tF0KGVKf0MOvzdlQIWLQ8o73S4pTv2N-F6UlzI0fdMtTHMLNNQyr0NdWdnuBk_jMBXO-gy5RE_oCRfMTTYRX2n3XLK6pTfXE0Ct338o9F5sH8Ph2lTXSu56cpdsfZOQZGqCH0LoFp1Dd7RJgIgNmBiTGfvDnA" },