8.1 ApiKeyMiddleware
Enforce X-Api-Key on /api/* requests with constant-time comparison.
/i/{mac}.png and /health remain unauthenticated. No-op when the
configured key is empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,14 +187,14 @@ Each type lives in `src/FrameProcessor/Domain/`. Tests in `tests/FrameProcessor.
|
|||||||
- Fetch → pipeline → store → publish. Same response shape as 5.1.
|
- Fetch → pipeline → store → publish. Same response shape as 5.1.
|
||||||
- Map `ImageFetchException` to `502 Bad Gateway`.
|
- Map `ImageFetchException` to `502 Bad Gateway`.
|
||||||
|
|
||||||
### [ ] 7.3 Manual check
|
### [x] 7.3 Manual check
|
||||||
- `curl -H "Content-Type: application/json" -d '{"url":"https://..."}' .../image-url` works end-to-end.
|
- `curl -H "Content-Type: application/json" -d '{"url":"https://..."}' .../image-url` works end-to-end.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 8 — Auth + concurrency + robustness
|
## Phase 8 — Auth + concurrency + robustness
|
||||||
|
|
||||||
### [ ] 8.1 `ApiKeyMiddleware`
|
### [x] 8.1 `ApiKeyMiddleware`
|
||||||
- Matches request path `/api/*`; reads `X-Api-Key` header; constant-time compare against `ApiKeyOptions` only if `ApiKeyOptions`is set to non-empty.
|
- Matches request path `/api/*`; reads `X-Api-Key` header; constant-time compare against `ApiKeyOptions` only if `ApiKeyOptions`is set to non-empty.
|
||||||
- 401 on mismatch. `/i/{mac}.png` and `/health` unaffected.
|
- 401 on mismatch. `/i/{mac}.png` and `/health` unaffected.
|
||||||
|
|
||||||
|
|||||||
48
src/FrameProcessor/Middleware/ApiKeyMiddleware.cs
Normal file
48
src/FrameProcessor/Middleware/ApiKeyMiddleware.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using FrameProcessor.Configuration;
|
||||||
|
using FrameProcessor.Domain;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FrameProcessor.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enforces an <c>X-Api-Key</c> header on <c>/api/*</c> requests. Other paths
|
||||||
|
/// (notably <c>/i/{mac}.png</c> and <c>/health</c>) pass through untouched.
|
||||||
|
/// When the configured key is empty the middleware is a no-op.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ApiKeyMiddleware
|
||||||
|
{
|
||||||
|
private const string HeaderName = "X-Api-Key";
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IOptionsMonitor<ApiKeyOptions> _options;
|
||||||
|
|
||||||
|
public ApiKeyMiddleware(RequestDelegate next, IOptionsMonitor<ApiKeyOptions> options)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
if (!context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await _next(context).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configured = _options.CurrentValue.Value;
|
||||||
|
if (string.IsNullOrEmpty(configured))
|
||||||
|
{
|
||||||
|
await _next(context).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var provided = context.Request.Headers[HeaderName].ToString();
|
||||||
|
if (!new ApiKey(configured).Matches(provided))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using FrameProcessor.Configuration;
|
using FrameProcessor.Configuration;
|
||||||
using FrameProcessor.ImagePipeline;
|
using FrameProcessor.ImagePipeline;
|
||||||
|
using FrameProcessor.Middleware;
|
||||||
using FrameProcessor.Mqtt;
|
using FrameProcessor.Mqtt;
|
||||||
using FrameProcessor.Storage;
|
using FrameProcessor.Storage;
|
||||||
using FrameProcessor.UrlFetch;
|
using FrameProcessor.UrlFetch;
|
||||||
@@ -64,6 +65,8 @@ var app = builder.Build();
|
|||||||
// Eagerly resolve FramesRegistry so an invalid frames.json fails startup fast.
|
// Eagerly resolve FramesRegistry so an invalid frames.json fails startup fast.
|
||||||
_ = app.Services.GetRequiredService<FramesRegistry>();
|
_ = app.Services.GetRequiredService<FramesRegistry>();
|
||||||
|
|
||||||
|
app.UseMiddleware<ApiKeyMiddleware>();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user