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.
This commit is contained in:
2026-10-20 11:21:18 +02:00
parent 048456ddc7
commit d07bc2418b
5 changed files with 31 additions and 3 deletions

View File

@@ -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;
/// <summary>
/// Validates required API key header for API endpoints.
/// </summary>
public sealed class ApiKeyAuthMiddleware(RequestDelegate next)
public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions<ApiKeyAuthOptions> apiKeyOptions)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly string? _configuredApiKey = apiKeyOptions.Value.Key;
/// <summary>
/// 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;

View File

@@ -0,0 +1,8 @@
namespace DigitalData.TaskFlow.API.Options;
public sealed class ApiKeyAuthOptions
{
public const string SectionName = "ApiKey";
public string? Key { get; init; }
}

View File

@@ -96,6 +96,10 @@ try
.AddOptions<SwaggerOptions>()
.Bind(config.GetSection(SwaggerOptions.SectionName));
builder.Services
.AddOptions<ApiKeyAuthOptions>()
.Bind(config.GetSection(ApiKeyAuthOptions.SectionName));
builder.Services.AddSerilogUi(options =>
{
options.UseSqliteServer(databaseOptions =>
@@ -110,11 +114,18 @@ try
var swaggerOptions = app.Services
.GetRequiredService<Microsoft.Extensions.Options.IOptions<SwaggerOptions>>()
.Value;
var apiKeyOptions = app.Services
.GetRequiredService<Microsoft.Extensions.Options.IOptions<ApiKeyAuthOptions>>()
.Value;
var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction;
var enforceApiKey = !string.IsNullOrWhiteSpace(apiKeyOptions.Key);
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseMiddleware<ApiKeyAuthMiddleware>();
if (enforceApiKey)
{
app.UseMiddleware<ApiKeyAuthMiddleware>();
}
app.UseSerilogRequestLogging();
if (enableSwagger)

View File

@@ -10,6 +10,9 @@
}
}
},
"ApiKey": {
"Key": null
},
"Logging": {
"LogLevel": {
"Default": "Information",

View File

@@ -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"
},