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:
@@ -1,6 +1,8 @@
|
|||||||
using DigitalData.TaskFlow.API.Contracts.Common;
|
using DigitalData.TaskFlow.API.Contracts.Common;
|
||||||
|
using DigitalData.TaskFlow.API.Options;
|
||||||
using DigitalData.TaskFlow.Domain.Common;
|
using DigitalData.TaskFlow.Domain.Common;
|
||||||
using DigitalData.TaskFlow.Domain.Enums;
|
using DigitalData.TaskFlow.Domain.Enums;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DigitalData.TaskFlow.API.Middleware;
|
namespace DigitalData.TaskFlow.API.Middleware;
|
||||||
@@ -8,9 +10,10 @@ namespace DigitalData.TaskFlow.API.Middleware;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates required API key header for API endpoints.
|
/// Validates required API key header for API endpoints.
|
||||||
/// </summary>
|
/// </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 static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private readonly string? _configuredApiKey = apiKeyOptions.Value.Key;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes middleware logic.
|
/// Executes middleware logic.
|
||||||
@@ -25,7 +28,7 @@ public sealed class ApiKeyAuthMiddleware(RequestDelegate next)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context.Request.Headers.TryGetValue("X-Api-Key", out var values)
|
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);
|
await next(context);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DigitalData.TaskFlow.API.Options;
|
||||||
|
|
||||||
|
public sealed class ApiKeyAuthOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "ApiKey";
|
||||||
|
|
||||||
|
public string? Key { get; init; }
|
||||||
|
}
|
||||||
@@ -96,6 +96,10 @@ try
|
|||||||
.AddOptions<SwaggerOptions>()
|
.AddOptions<SwaggerOptions>()
|
||||||
.Bind(config.GetSection(SwaggerOptions.SectionName));
|
.Bind(config.GetSection(SwaggerOptions.SectionName));
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddOptions<ApiKeyAuthOptions>()
|
||||||
|
.Bind(config.GetSection(ApiKeyAuthOptions.SectionName));
|
||||||
|
|
||||||
builder.Services.AddSerilogUi(options =>
|
builder.Services.AddSerilogUi(options =>
|
||||||
{
|
{
|
||||||
options.UseSqliteServer(databaseOptions =>
|
options.UseSqliteServer(databaseOptions =>
|
||||||
@@ -110,11 +114,18 @@ try
|
|||||||
var swaggerOptions = app.Services
|
var swaggerOptions = app.Services
|
||||||
.GetRequiredService<Microsoft.Extensions.Options.IOptions<SwaggerOptions>>()
|
.GetRequiredService<Microsoft.Extensions.Options.IOptions<SwaggerOptions>>()
|
||||||
.Value;
|
.Value;
|
||||||
|
var apiKeyOptions = app.Services
|
||||||
|
.GetRequiredService<Microsoft.Extensions.Options.IOptions<ApiKeyAuthOptions>>()
|
||||||
|
.Value;
|
||||||
|
|
||||||
var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction;
|
var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction;
|
||||||
|
var enforceApiKey = !string.IsNullOrWhiteSpace(apiKeyOptions.Key);
|
||||||
|
|
||||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
app.UseMiddleware<ApiKeyAuthMiddleware>();
|
if (enforceApiKey)
|
||||||
|
{
|
||||||
|
app.UseMiddleware<ApiKeyAuthMiddleware>();
|
||||||
|
}
|
||||||
app.UseSerilogRequestLogging();
|
app.UseSerilogRequestLogging();
|
||||||
|
|
||||||
if (enableSwagger)
|
if (enableSwagger)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ApiKey": {
|
||||||
|
"Key": null
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"TaskFlowDb": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;"
|
"TaskFlowDb": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;"
|
||||||
},
|
},
|
||||||
|
"ApiKey": {
|
||||||
|
"Key": null
|
||||||
|
},
|
||||||
"MediatR": {
|
"MediatR": {
|
||||||
"LuckyPennySoftLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxODE2MTI4MDAwIiwiaWF0IjoiMTc4NDYyNDU1NyIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiMDE5ODNjNTllNGIzNzI4ZWJmZDMxMDNjMmE0ODZlODUiLCJzdWJfaWQiOiItIiwiZWRpdGlvbiI6IjAiLCJ0eXBlIjoiMiJ9.IUUO926m9crYGYxMjjKD_n9BnUm-EDyjFIn0YmMUCo7C-QTwvB8WhXP8veTSFsBq-leIIDJ4jyl7Pgc_7ciwg1XhUSIs4mkQroEUaSFCGOxw7Pi41WM8MK5YFSaqLTYYXec9zxgiJbGzABbh3CHTSup3okGnVm_CMoPEs91l2c0A6N1JyZy74urd_tF0KGVKf0MOvzdlQIWLQ8o73S4pTv2N-F6UlzI0fdMtTHMLNNQyr0NdWdnuBk_jMBXO-gy5RE_oCRfMTTYRX2n3XLK6pTfXE0Ct338o9F5sH8Ph2lTXSu56cpdsfZOQZGqCH0LoFp1Dd7RJgIgNmBiTGfvDnA"
|
"LuckyPennySoftLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxODE2MTI4MDAwIiwiaWF0IjoiMTc4NDYyNDU1NyIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiMDE5ODNjNTllNGIzNzI4ZWJmZDMxMDNjMmE0ODZlODUiLCJzdWJfaWQiOiItIiwiZWRpdGlvbiI6IjAiLCJ0eXBlIjoiMiJ9.IUUO926m9crYGYxMjjKD_n9BnUm-EDyjFIn0YmMUCo7C-QTwvB8WhXP8veTSFsBq-leIIDJ4jyl7Pgc_7ciwg1XhUSIs4mkQroEUaSFCGOxw7Pi41WM8MK5YFSaqLTYYXec9zxgiJbGzABbh3CHTSup3okGnVm_CMoPEs91l2c0A6N1JyZy74urd_tF0KGVKf0MOvzdlQIWLQ8o73S4pTv2N-F6UlzI0fdMtTHMLNNQyr0NdWdnuBk_jMBXO-gy5RE_oCRfMTTYRX2n3XLK6pTfXE0Ct338o9F5sH8Ph2lTXSu56cpdsfZOQZGqCH0LoFp1Dd7RJgIgNmBiTGfvDnA"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user