diff --git a/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs b/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs index e7be45050..17cafa25d 100644 --- a/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs +++ b/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs @@ -1,21 +1,26 @@ +using System.Security.Claims; using LancacheManager.Core.Interfaces; using LancacheManager.Security; namespace LancacheManager.Middleware; /// -/// Middleware to optionally require API key authentication for Prometheus metrics endpoint (/metrics) +/// Middleware to optionally require API key authentication for Prometheus metrics endpoint (/metrics). +/// +/// This middleware MUST be registered before UseAuthorization() in Program.cs so it can set +/// context.User before the FallbackPolicy (RequireAuthenticatedUser) is evaluated. /// /// Configuration Priority: /// 1. UI Toggle (StateService) - if set via UI, takes precedence /// 2. Environment Variable / appsettings.json (Security:RequireAuthForMetrics) - default fallback /// /// Values: -/// - false (default): Metrics are PUBLIC - no authentication required -/// - true: Metrics require API Key in X-Api-Key header +/// - false (default): Metrics are PUBLIC - a synthetic principal is set so the FallbackPolicy +/// is satisfied without requiring any credentials from the scraper. +/// - true: Metrics require an API key via X-Api-Key header or Authorization: Bearer <key>. /// /// Use Cases: -/// - false: Prometheus/Grafana can scrape metrics without authentication (common setup) +/// - false: Prometheus/Grafana can scrape metrics without authentication (common LAN setup) /// - true: Secure metrics endpoint with API key (for internet-exposed instances) /// public class MetricsAuthenticationMiddleware @@ -51,15 +56,18 @@ public async Task InvokeAsync(HttpContext context, AuthenticationHelper authHelp if (!requireAuth) { - // Metrics are public - allow Prometheus/Grafana to scrape without authentication + // Metrics are public — set a synthetic authenticated principal so the + // FallbackPolicy (RequireAuthenticatedUser) does not reject the request. + context.User = CreateMetricsPrincipal(); await _next(context); return; } - // Metrics require authentication - check for API key + // Metrics require authentication — validate API key (X-Api-Key or Authorization: Bearer). var result = authHelper.ValidateApiKey(context); if (result.IsAuthenticated) { + context.User = CreateMetricsPrincipal(); await _next(context); return; } @@ -70,4 +78,9 @@ public async Task InvokeAsync(HttpContext context, AuthenticationHelper authHelp await AuthenticationHelper.WriteErrorResponseAsync( context, result.StatusCode, result.ErrorMessage ?? "API key required for metrics"); } + + private static ClaimsPrincipal CreateMetricsPrincipal() => + new(new ClaimsIdentity( + [new Claim(ClaimTypes.Name, "prometheus-scraper")], + authenticationType: "Metrics")); } diff --git a/Api/LancacheManager/Program.cs b/Api/LancacheManager/Program.cs index bfcc7a692..ce70d8226 100644 --- a/Api/LancacheManager/Program.cs +++ b/Api/LancacheManager/Program.cs @@ -780,11 +780,14 @@ // ASP.NET Core authentication & authorization pipeline // SessionAuthenticationHandler populates HttpContext.Items["Session"] for backward compatibility. app.UseAuthentication(); -app.UseAuthorization(); -// Add Metrics Authentication Middleware (optional API key for /metrics) +// MetricsAuthenticationMiddleware MUST run before UseAuthorization so it can set context.User +// before the FallbackPolicy (RequireAuthenticatedUser) is evaluated. When RequireAuthForMetrics +// is false it sets a synthetic principal; when true it validates the API key first. app.UseMiddleware(); +app.UseAuthorization(); + // Swagger authentication middleware (requires API key when Security:ProtectSwagger=true) app.UseMiddleware(); diff --git a/Api/LancacheManager/Security/AuthenticationHelper.cs b/Api/LancacheManager/Security/AuthenticationHelper.cs index eb6fffb21..44c224595 100644 --- a/Api/LancacheManager/Security/AuthenticationHelper.cs +++ b/Api/LancacheManager/Security/AuthenticationHelper.cs @@ -71,10 +71,21 @@ public bool IsAuthenticated(HttpContext context) /// /// Gets the API key from request headers. + /// Accepts X-Api-Key header (primary) or Authorization: Bearer <key> (Prometheus convention). /// public static string? GetApiKeyFromHeader(HttpContext context) { - return context.Request.Headers["X-Api-Key"].FirstOrDefault(); + var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault(); + if (apiKey != null) + return apiKey; + + // Support Authorization: Bearer so Prometheus scrape_config can use the + // standard `authorization: { type: Bearer, credentials: }` block. + var authHeader = context.Request.Headers.Authorization.FirstOrDefault(); + if (authHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true) + return authHeader["Bearer ".Length..].Trim(); + + return null; } /// diff --git a/README.MD b/README.MD index dae012d04..3d72d7dfc 100644 --- a/README.MD +++ b/README.MD @@ -31,6 +31,7 @@ A web UI for [LANCache](https://lancache.net/). Watch downloads as they happen, - [Reverse Proxy (Nginx)](#nginx-reverse-proxy) - [Monitoring (Grafana & Prometheus)](#grafana--prometheus) - [Troubleshooting](#troubleshooting) + - [/metrics returns 401 despite env var](#metrics-401) - [Building from Source](#building-from-source) - [Contributing Translations](#contributing-translations) - [Support and License](#support-and-license) @@ -300,7 +301,7 @@ services: | `Security__EnableAuthentication` | `true` | Require an API key for admin actions. Only turn off for local dev. | | `Security__MaxAdminDevices` | `3` | How many devices can share the same API key. | | `Security__GuestSessionDurationHours` | `6` | Default guest session length (also configurable in the UI). | -| `Security__RequireAuthForMetrics` | `false` | Require an API key on `/metrics`. | +| `Security__RequireAuthForMetrics` | `false` | Require an API key on `/metrics`. **This env var is the default only** — if the toggle has ever been saved via the UI (Management → Security), the persisted value takes priority. See [Metrics returns 401 despite env var](#metrics-401) if the env var appears to have no effect. | | `Security__ProtectSwagger` | `true` | Require auth on Swagger docs in production. | | `Security__AllowedOrigins` | (empty) | Comma-separated CORS allow list. Empty allows all. | | `Security__AllowedBrowsePaths` | (empty) | Comma-separated allow-list of file-browser roots. Empty disables the file browser entirely (every request returns `403`). Example: `/data,/mnt`. | @@ -831,6 +832,45 @@ lancache_cache_size_bytes / 1024 / 1024 / 1024 2. The mapping service queries the Epic API to identify what's in your cache. 3. Game names and cover art come down automatically. + +### `/metrics` returns 401 despite `Security__RequireAuthForMetrics=false` + +**Symptom:** Prometheus (or a manual `curl`) gets a `401 Unauthorized` from `/metrics` even though `Security__RequireAuthForMetrics=false` is set in the environment and is visible in `docker inspect`. + +**Why it happens — config priority:** + +``` +UI toggle (persisted in state.json) > env var / appsettings.json default +``` + +`Security__RequireAuthForMetrics` is the *default fallback*. Once the toggle is saved via the UI (Management → Security → "Require authentication for metrics"), that value is written to `data/state/state.json` and permanently wins — the env var is no longer consulted. + +**Fix option 1 — via the UI:** + +Go to **Management → Security** and toggle "Require authentication for metrics" off, then save. + +**Fix option 2 — edit `state.json` directly** (when the UI is inaccessible or you prefer config-as-code): + +```bash +# Stop the container first to avoid a concurrent write race +docker stop lancache-manager + +# Null out the field so the env var becomes the effective value again +python3 -c " +import json +with open('./data/state/state.json') as f: + s = json.load(f) +s['requireAuthForMetrics'] = None +with open('./data/state/state.json', 'w') as f: + json.dump(s, f, indent=2) +print('done') +" + +docker start lancache-manager +``` + +After the restart, `Security__RequireAuthForMetrics=false` takes effect. + ### Lost API key ```bash