From 1afa68064e11df2e0e06efffa3f11c7a75680a20 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Sat, 31 Jan 2026 20:01:59 -0500 Subject: [PATCH] add API key authentication to Spotify admin endpoints --- allstarr/Controllers/JellyfinController.cs | 6 ++- allstarr/Filters/ApiKeyAuthFilter.cs | 52 ++++++++++++++++++++++ allstarr/Program.cs | 1 + 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 allstarr/Filters/ApiKeyAuthFilter.cs diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index ed48ac6..ea3d893 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1979,9 +1979,10 @@ public class JellyfinController : ControllerBase /// /// Manual trigger endpoint to force fetch Spotify missing tracks. - /// GET /spotify/sync + /// GET /spotify/sync?api_key=YOUR_KEY /// [HttpGet("spotify/sync")] + [ServiceFilter(typeof(ApiKeyAuthFilter))] public async Task TriggerSpotifySync() { if (!_spotifySettings.Enabled) @@ -2110,9 +2111,10 @@ public class JellyfinController : ControllerBase /// /// Clear Spotify playlist cache to force re-matching. - /// GET /spotify/clear-cache + /// GET /spotify/clear-cache?api_key=YOUR_KEY /// [HttpGet("spotify/clear-cache")] + [ServiceFilter(typeof(ApiKeyAuthFilter))] public async Task ClearSpotifyCache() { if (!_spotifySettings.Enabled) diff --git a/allstarr/Filters/ApiKeyAuthFilter.cs b/allstarr/Filters/ApiKeyAuthFilter.cs new file mode 100644 index 0000000..ef403e4 --- /dev/null +++ b/allstarr/Filters/ApiKeyAuthFilter.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using allstarr.Models.Settings; + +namespace allstarr.Filters; + +/// +/// Simple API key authentication filter for admin endpoints. +/// Validates against Jellyfin API key via query parameter or header. +/// +public class ApiKeyAuthFilter : IAsyncActionFilter +{ + private readonly JellyfinSettings _settings; + private readonly ILogger _logger; + + public ApiKeyAuthFilter( + IOptions settings, + ILogger logger) + { + _settings = settings.Value; + _logger = logger; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var request = context.HttpContext.Request; + + // Extract API key from query parameter or header + var apiKey = request.Query["api_key"].FirstOrDefault() + ?? request.Headers["X-Api-Key"].FirstOrDefault() + ?? request.Headers["X-Emby-Token"].FirstOrDefault(); + + // Validate API key + if (string.IsNullOrEmpty(apiKey) || !string.Equals(apiKey, _settings.ApiKey, StringComparison.Ordinal)) + { + _logger.LogWarning("Unauthorized access attempt to {Path} from {IP}", + request.Path, + context.HttpContext.Connection.RemoteIpAddress); + + context.Result = new UnauthorizedObjectResult(new + { + error = "Unauthorized", + message = "Valid API key required. Provide via ?api_key=YOUR_KEY or X-Api-Key header." + }); + return; + } + + _logger.LogDebug("API key authentication successful for {Path}", request.Path); + await next(); + } +} diff --git a/allstarr/Program.cs b/allstarr/Program.cs index 5234713..44c1684 100644 --- a/allstarr/Program.cs +++ b/allstarr/Program.cs @@ -174,6 +174,7 @@ if (backendType == BackendType.Jellyfin) builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); } else {