fix(jellyfin): preserve v12 auth and admin sessions
Docker Build & Push / build-and-test (push) Canceled after 0s
Docker Build & Push / docker (push) Canceled after 0s

This commit is contained in:
joshpatra committed 2026-09-23 20:16:27 -04:00
1 parent 9887fd69d5
commit c9313d6ac7
18 files changed
+257 -148

No files matched your search

+4
View File
@@ -213,6 +213,10 @@ Choose your preferred provider via the `MUSIC_SERVICE` environment variable. Add
JELLYFIN_LIBRARY_ID=
```
Jellyfin 12 requires the standard `Authorization: MediaBrowser ...` header.
Allstarr forwards a client's login identity separately from its optional
server-side API key, and uses the standard header for API-key requests.
`localhost` inside the Allstarr container points to Allstarr itself, not to
Jellyfin. The Compose file maps `host.docker.internal` on Linux, macOS, and
Windows for a Jellyfin server running on the Docker host.
@@ -32,6 +32,7 @@ public class AdminAuthenticationMiddlewareTests
Assert.False(nextInvoked);
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
Assert.Equal("true", context.Response.Headers["X-Allstarr-Session-Expired"]);
var body = await ReadResponseBodyAsync(context);
Assert.Contains("Authentication required", body);
@@ -70,6 +71,7 @@ public class AdminAuthenticationMiddlewareTests
Assert.True(nextInvoked);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
Assert.True(context.Items.ContainsKey(AdminAuthSessionService.HttpContextSessionItemKey));
Assert.False(context.Response.Headers.ContainsKey("X-Allstarr-Session-Expired"));
}
[Fact]
+29
View File
@@ -0,0 +1,29 @@
using allstarr.Models.Settings;
using allstarr.Services.Admin;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace allstarr.Tests;
public class AdminHelperServiceTests
{
[Fact]
public void CreateJellyfinRequest_UsesStandardAuthorizationForApiKey()
{
var environment = new Mock<IWebHostEnvironment>();
environment.SetupGet(value => value.EnvironmentName).Returns("Production");
var helper = new AdminHelperService(
NullLogger<AdminHelperService>.Instance,
Options.Create(new JellyfinSettings { ApiKey = "test-api-key" }),
environment.Object);
using var request = helper.CreateJellyfinRequest(HttpMethod.Get, "http://jellyfin.local/Users");
var authorization = Assert.Single(request.Headers.GetValues("Authorization"));
Assert.Contains("Client=\"Allstarr\"", authorization);
Assert.Contains("Token=\"test-api-key\"", authorization);
Assert.False(request.Headers.Contains("X-Emby-Authorization"));
}
}
+39 -8
View File
@@ -7,25 +7,27 @@ namespace allstarr.Tests;
public class AuthHeaderHelperTests
{
[Fact]
public void ForwardAuthHeaders_ShouldPreferXEmbyAuthorization()
public void ForwardAuthHeaders_ShouldPreferStandardAuthorization()
{
var headers = new HeaderDictionary
{
["X-Emby-Authorization"] = "MediaBrowser Token=\"abc\"",
["Authorization"] = "Bearer xyz"
["X-Emby-Authorization"] = "MediaBrowser Token=\"old\"",
["X-Emby-Token"] = "old",
["Authorization"] = "MediaBrowser Client=\"Yuzic\", Device=\"Phone\", DeviceId=\"device-1\", Version=\"2.0\""
};
using var request = new HttpRequestMessage();
var forwarded = AuthHeaderHelper.ForwardAuthHeaders(headers, request);
Assert.True(forwarded);
Assert.True(request.Headers.TryGetValues("X-Emby-Authorization", out var values));
Assert.Contains("MediaBrowser Token=\"abc\"", values);
Assert.False(request.Headers.Contains("Authorization"));
Assert.True(request.Headers.TryGetValues("Authorization", out var values));
Assert.Contains("MediaBrowser Client=\"Yuzic\", Device=\"Phone\", DeviceId=\"device-1\", Version=\"2.0\"", values);
Assert.False(request.Headers.Contains("X-Emby-Authorization"));
Assert.False(request.Headers.Contains("X-Emby-Token"));
}
[Fact]
public void ForwardAuthHeaders_ShouldMapMediaBrowserAuthorizationToXEmby()
public void ForwardAuthHeaders_ShouldPreserveMediaBrowserAuthorization()
{
var headers = new HeaderDictionary
{
@@ -36,7 +38,36 @@ public class AuthHeaderHelperTests
var forwarded = AuthHeaderHelper.ForwardAuthHeaders(headers, request);
Assert.True(forwarded);
Assert.True(request.Headers.Contains("X-Emby-Authorization"));
Assert.Equal(headers["Authorization"].ToString(), request.Headers.GetValues("Authorization").Single());
Assert.False(request.Headers.Contains("X-Emby-Authorization"));
}
[Fact]
public void ForwardAuthHeaders_ShouldUpgradeLegacyMediaBrowserHeader()
{
var headers = new HeaderDictionary
{
["X-Emby-Authorization"] = "MediaBrowser Client=\"OlderClient\", Device=\"Phone\", DeviceId=\"device-2\", Version=\"1.0\""
};
using var request = new HttpRequestMessage();
Assert.True(AuthHeaderHelper.ForwardAuthHeaders(headers, request));
Assert.Equal(headers["X-Emby-Authorization"].ToString(), request.Headers.GetValues("Authorization").Single());
Assert.False(request.Headers.Contains("X-Emby-Authorization"));
}
[Fact]
public void ForwardAuthHeaders_ShouldKeepLegacyEmbySchemeForOlderServers()
{
var headers = new HeaderDictionary
{
["X-Emby-Authorization"] = "Emby Client=\"OlderClient\""
};
using var request = new HttpRequestMessage();
Assert.True(AuthHeaderHelper.ForwardAuthHeaders(headers, request));
Assert.Equal(headers["X-Emby-Authorization"].ToString(), request.Headers.GetValues("X-Emby-Authorization").Single());
Assert.False(request.Headers.Contains("Authorization"));
}
[Fact]
+1
View File
@@ -142,6 +142,7 @@ public class JavaScriptSyntaxTests
Assert.Contains("window.openEditSetting", settingsContent);
Assert.Contains("window.saveEditSetting", settingsContent);
Assert.Contains("window.logoutAdminSession", authContent);
Assert.Contains("response.headers.get(\"X-Allstarr-Session-Expired\") === \"true\"", authContent);
Assert.Contains("window.restartContainer", operationsContent);
Assert.Contains("window.linkPlaylist", playlistContent);
Assert.Contains("window.loadScrobblingConfig", scrobblingContent);
@@ -0,0 +1,73 @@
using System.Net;
using System.Text;
using allstarr.Controllers;
using allstarr.Models.Settings;
using allstarr.Services.Common;
using allstarr.Services.Jellyfin;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace allstarr.Tests;
public class JellyfinAuthenticationProxyTests
{
[Fact]
public async Task AuthenticateByName_PreservesJellyfinErrorStatusAndClientIdentity()
{
string? forwardedAuthorization = null;
var handler = new StubHandler(request =>
{
forwardedAuthorization = request.Headers.GetValues("Authorization").Single();
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("{\"error\":\"client identity required\"}")
};
});
var clientFactory = new Mock<IHttpClientFactory>();
clientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient(handler));
var settings = Options.Create(new JellyfinSettings { Url = "http://jellyfin.local" });
var context = new DefaultHttpContext();
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"Username\":\"josh\",\"Pw\":\"test\"}"));
context.Request.Headers.Authorization = "MediaBrowser Client=\"Yuzic\", Device=\"Phone\", DeviceId=\"device-1\", Version=\"2.0\"";
var cache = new RedisCacheService(
Options.Create(new RedisSettings { Enabled = false }),
NullLogger<RedisCacheService>.Instance);
var proxy = new JellyfinProxyService(
clientFactory.Object,
settings,
new HttpContextAccessor { HttpContext = context },
NullLogger<JellyfinProxyService>.Instance,
cache);
var controller = new JellyfinController(
settings,
Options.Create(new SpotifyImportSettings()),
Options.Create(new SpotifyApiSettings()),
Options.Create(new ScrobblingSettings()),
null!, null!, null!, null!, null!,
proxy, null!, null!, cache,
new ConfigurationBuilder().Build(),
NullLogger<JellyfinController>.Instance)
{
ControllerContext = new ControllerContext { HttpContext = context }
};
var result = await controller.AuthenticateByName();
var content = Assert.IsType<ContentResult>(result);
Assert.Equal(StatusCodes.Status400BadRequest, content.StatusCode);
Assert.Equal("{\"error\":\"client identity required\"}", content.Content);
Assert.Equal(context.Request.Headers.Authorization.ToString(), forwardedAuthorization);
}
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> send) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) => Task.FromResult(send(request));
}
}
+24 -1
View File
@@ -146,6 +146,28 @@ public class JellyfinProxyServiceTests
Assert.Contains("token-123", values);
}
[Fact]
public async Task GetJsonAsyncInternal_WithServerApiKey_UsesStandardAuthorization()
{
HttpRequestMessage? captured = null;
_mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.Callback<HttpRequestMessage, CancellationToken>((req, _) => captured = req)
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{}")
});
await _service.GetJsonAsyncInternal("Items");
Assert.NotNull(captured);
Assert.Contains("Client=\"TestClient\"", captured!.Headers.GetValues("Authorization").Single());
Assert.Contains("Token=\"test-api-key-12345\"", captured.Headers.GetValues("Authorization").Single());
Assert.False(captured.Headers.Contains("X-Emby-Authorization"));
}
[Fact]
public async Task GetBytesAsync_ReturnsBodyAndContentType()
{
@@ -367,8 +389,9 @@ public class JellyfinProxyServiceTests
// Assert
Assert.NotNull(captured);
Assert.True(captured!.Headers.TryGetValues("X-Emby-Authorization", out var values));
Assert.True(captured!.Headers.TryGetValues("Authorization", out var values));
Assert.Contains("MediaBrowser Token=\"abc\"", values);
Assert.False(captured.Headers.Contains("X-Emby-Authorization"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
+1 -1
View File
@@ -9,5 +9,5 @@ public static class AppVersion
/// <summary>
/// Current application version.
/// </summary>
public const string Version = "2.0.3";
public const string Version = "2.0.4";
}
@@ -95,8 +95,7 @@ public class JellyfinAdminController : ControllerBase
var request = new HttpRequestMessage(method, url);
var authHeader =
$"MediaBrowser Client=\"AllstarrAdmin\", Device=\"WebUI\", DeviceId=\"allstarr-admin-webui\", Version=\"{AppVersion.Version}\", Token=\"{session.JellyfinAccessToken}\"";
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", authHeader);
request.Headers.TryAddWithoutValidation("X-Emby-Token", session.JellyfinAccessToken);
request.Headers.TryAddWithoutValidation("Authorization", authHeader);
return request;
}
@@ -49,7 +49,12 @@ public partial class JellyfinController
}
// Return Jellyfin's exact response
return Content(responseJson, "application/json");
return new ContentResult
{
Content = responseJson,
ContentType = "application/json",
StatusCode = statusCode
};
}
// No response body from Jellyfin - return status code only
@@ -103,6 +103,7 @@ public class AdminAuthenticationMiddleware
context.Request.Path);
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers["X-Allstarr-Session-Expired"] = "true";
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
@@ -2,6 +2,7 @@ using System.Net.WebSockets;
using Microsoft.Extensions.Options;
using allstarr.Models.Settings;
using allstarr.Services.Jellyfin;
using allstarr.Services.Common;
namespace allstarr.Middleware;
@@ -113,32 +114,11 @@ public class WebSocketProxyMiddleware
// Connect to Jellyfin WebSocket
serverWebSocket = new ClientWebSocket();
// Forward authentication headers - check X-Emby-Authorization FIRST
// Most Jellyfin clients use X-Emby-Authorization, not Authorization
if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
var auth = AuthHeaderHelper.GetForwardAuthHeader(context.Request.Headers);
if (auth is { } selected)
{
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuthHeader.ToString());
_logger.LogDebug("🔑 WEBSOCKET: Forwarded X-Emby-Authorization header");
}
else if (context.Request.Headers.TryGetValue("X-Emby-Token", out var tokenHeader))
{
serverWebSocket.Options.SetRequestHeader("X-Emby-Token", tokenHeader.ToString());
_logger.LogDebug("🔑 WEBSOCKET: Forwarded X-Emby-Token header");
}
else if (context.Request.Headers.TryGetValue("Authorization", out var authHeader2))
{
var authValue = authHeader2.ToString();
// If it's a MediaBrowser auth header, use X-Emby-Authorization
if (authValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase))
{
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", authValue);
_logger.LogDebug("🔑 WEBSOCKET: Converted Authorization to X-Emby-Authorization header");
}
else
{
serverWebSocket.Options.SetRequestHeader("Authorization", authValue);
_logger.LogDebug("🔑 WEBSOCKET: Forwarded Authorization header");
}
serverWebSocket.Options.SetRequestHeader(selected.Name, selected.Value);
_logger.LogDebug("🔑 WEBSOCKET: Forwarded {HeaderName} header", selected.Name);
}
// Set user agent
@@ -582,7 +582,7 @@ public class AdminHelperService
public HttpRequestMessage CreateJellyfinRequest(HttpMethod method, string url)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
request.Headers.Add("Authorization", GetJellyfinAuthHeader());
return request;
}
+52 -72
View File
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace allstarr.Services.Common;
@@ -18,56 +17,37 @@ public static class AuthHeaderHelper
/// <returns>True if auth header was added, false otherwise</returns>
public static bool ForwardAuthHeaders(IHeaderDictionary sourceHeaders, HttpRequestMessage targetRequest)
{
// Try X-Emby-Authorization first (case-insensitive)
foreach (var header in sourceHeaders)
{
if (header.Key.Equals("X-Emby-Authorization", StringComparison.OrdinalIgnoreCase))
{
var headerValue = header.Value.ToString();
targetRequest.Headers.TryAddWithoutValidation("X-Emby-Authorization", headerValue);
return true;
}
}
// Some Jellyfin clients send the raw token separately instead of a MediaBrowser auth header.
foreach (var header in sourceHeaders)
{
if (header.Key.Equals("X-Emby-Token", StringComparison.OrdinalIgnoreCase))
{
var headerValue = header.Value.ToString();
targetRequest.Headers.TryAddWithoutValidation("X-Emby-Token", headerValue);
return true;
}
}
// If no X-Emby-Authorization, check if Authorization header contains MediaBrowser format
foreach (var header in sourceHeaders)
{
if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase))
{
var headerValue = header.Value.ToString();
// Check if it's a MediaBrowser/Jellyfin auth header
if (headerValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase) ||
headerValue.Contains("Client=", StringComparison.OrdinalIgnoreCase) ||
headerValue.Contains("Token=", StringComparison.OrdinalIgnoreCase))
{
// Forward as X-Emby-Authorization (Jellyfin's expected header)
targetRequest.Headers.TryAddWithoutValidation("X-Emby-Authorization", headerValue);
return true;
}
else
{
// Standard Bearer token
targetRequest.Headers.TryAddWithoutValidation("Authorization", headerValue);
return true;
}
}
}
return false;
var auth = GetForwardAuthHeader(sourceHeaders);
return auth is { } selected &&
targetRequest.Headers.TryAddWithoutValidation(selected.Name, selected.Value);
}
public static (string Name, string Value)? GetForwardAuthHeader(IHeaderDictionary sourceHeaders)
{
if (sourceHeaders.TryGetValue("Authorization", out var authorization) &&
!string.IsNullOrWhiteSpace(authorization))
{
return ("Authorization", authorization.ToString());
}
if (sourceHeaders.TryGetValue("X-Emby-Authorization", out var legacyAuthorization) &&
!string.IsNullOrWhiteSpace(legacyAuthorization))
{
var value = legacyAuthorization.ToString();
return value.StartsWith("MediaBrowser ", StringComparison.OrdinalIgnoreCase)
? ("Authorization", value)
: ("X-Emby-Authorization", value);
}
if (sourceHeaders.TryGetValue("X-Emby-Token", out var legacyToken) &&
!string.IsNullOrWhiteSpace(legacyToken))
{
return ("X-Emby-Token", legacyToken.ToString());
}
return null;
}
/// <summary>
/// Extracts device ID from X-Emby-Authorization header.
/// </summary>
@@ -80,7 +60,7 @@ public static class AuthHeaderHelper
var authValue = authHeader.ToString();
return ExtractDeviceIdFromAuthString(authValue);
}
if (headers.TryGetValue("Authorization", out var authHeader2))
{
var authValue = authHeader2.ToString();
@@ -89,10 +69,10 @@ public static class AuthHeaderHelper
return ExtractDeviceIdFromAuthString(authValue);
}
}
return null;
}
/// <summary>
/// Extracts device ID from MediaBrowser auth string.
/// Format: MediaBrowser Client="...", Device="...", DeviceId="...", Version="...", Token="..."
@@ -100,18 +80,18 @@ public static class AuthHeaderHelper
private static string? ExtractDeviceIdFromAuthString(string authValue)
{
var deviceIdMatch = System.Text.RegularExpressions.Regex.Match(
authValue,
@"DeviceId=""([^""]+)""",
authValue,
@"DeviceId=""([^""]+)""",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (deviceIdMatch.Success)
{
return deviceIdMatch.Groups[1].Value;
}
return null;
}
/// <summary>
/// Extracts client name from MediaBrowser auth string.
/// </summary>
@@ -122,7 +102,7 @@ public static class AuthHeaderHelper
var authValue = authHeader.ToString();
return ExtractClientNameFromAuthString(authValue);
}
if (headers.TryGetValue("Authorization", out var authHeader2))
{
var authValue = authHeader2.ToString();
@@ -131,49 +111,49 @@ public static class AuthHeaderHelper
return ExtractClientNameFromAuthString(authValue);
}
}
return null;
}
/// <summary>
/// Extracts client name from MediaBrowser auth string.
/// </summary>
private static string? ExtractClientNameFromAuthString(string authValue)
{
var clientMatch = System.Text.RegularExpressions.Regex.Match(
authValue,
@"Client=""([^""]+)""",
authValue,
@"Client=""([^""]+)""",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (clientMatch.Success)
{
return clientMatch.Groups[1].Value;
}
return null;
}
/// <summary>
/// Creates a MediaBrowser auth header string.
/// </summary>
public static string CreateAuthHeader(string token, string? client = null, string? device = null, string? deviceId = null, string? version = null)
{
var parts = new List<string>();
if (!string.IsNullOrEmpty(client))
parts.Add($"Client=\"{client}\"");
if (!string.IsNullOrEmpty(device))
parts.Add($"Device=\"{device}\"");
if (!string.IsNullOrEmpty(deviceId))
parts.Add($"DeviceId=\"{deviceId}\"");
if (!string.IsNullOrEmpty(version))
parts.Add($"Version=\"{version}\"");
parts.Add($"Token=\"{token}\"");
return $"MediaBrowser {string.Join(", ", parts)}";
}
}
@@ -1013,7 +1013,7 @@ public class JellyfinProxyService
// Use server's API key for authentication
var authHeader = GetAuthorizationHeader();
request.Headers.TryAddWithoutValidation("X-Emby-Authorization", authHeader);
request.Headers.TryAddWithoutValidation("Authorization", authHeader);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@@ -5,6 +5,7 @@ using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using allstarr.Models.Settings;
using allstarr.Services.Common;
namespace allstarr.Services.Jellyfin;
@@ -503,9 +504,7 @@ public class JellyfinSessionManager : IDisposable
var jellyfinHost = jellyfinUrl.Replace("https://", "").Replace("http://", "");
var jellyfinWsUrl = $"{wsScheme}{jellyfinHost}/socket";
// IMPORTANT: Do NOT add api_key to URL - we want to authenticate as the CLIENT, not the server
// The client's token is passed via X-Emby-Authorization header
// Using api_key would create a session for the server/admin, not the actual user's client
// Use the client's identity when available; URL query credentials are not needed.
webSocket = new ClientWebSocket();
session.WebSocket = webSocket;
@@ -517,44 +516,25 @@ public class JellyfinSessionManager : IDisposable
_logger.LogDebug("🔍 WEBSOCKET: Available headers for {DeviceId}: {Headers}",
deviceId, string.Join(", ", sessionHeaders.Keys));
// Forward authentication headers from the CLIENT - this is critical for session to appear under the right user
bool authFound = false;
if (sessionHeaders.TryGetValue("X-Emby-Authorization", out var embyAuth))
// Keep the same client identity used for proxied HTTP requests.
var auth = AuthHeaderHelper.GetForwardAuthHeader(sessionHeaders);
if (auth is { } selected)
{
webSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuth.ToString());
_logger.LogDebug("🔑 WEBSOCKET: Using X-Emby-Authorization for {DeviceId}", deviceId);
authFound = true;
}
else if (sessionHeaders.TryGetValue("X-Emby-Token", out var token))
{
webSocket.Options.SetRequestHeader("X-Emby-Token", token.ToString());
_logger.LogDebug("🔑 WEBSOCKET: Using X-Emby-Token for {DeviceId}", deviceId);
authFound = true;
}
else if (sessionHeaders.TryGetValue("Authorization", out var auth))
{
var authValue = auth.ToString();
if (authValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase))
{
webSocket.Options.SetRequestHeader("X-Emby-Authorization", authValue);
_logger.LogDebug("🔑 WEBSOCKET: Converted Authorization to X-Emby-Authorization for {DeviceId}",
deviceId);
authFound = true;
}
else
{
webSocket.Options.SetRequestHeader("Authorization", authValue);
_logger.LogDebug("🔑 WEBSOCKET: Using Authorization for {DeviceId}", deviceId);
authFound = true;
}
webSocket.Options.SetRequestHeader(selected.Name, selected.Value);
_logger.LogDebug("🔑 WEBSOCKET: Using {HeaderName} for {DeviceId}", selected.Name, deviceId);
}
if (!authFound)
if (auth is null)
{
// No client auth found - fall back to server API key as last resort
// Preserve the existing server-key fallback without a legacy query credential.
if (!string.IsNullOrEmpty(_settings.ApiKey))
{
jellyfinWsUrl += $"?api_key={_settings.ApiKey}";
webSocket.Options.SetRequestHeader("Authorization", AuthHeaderHelper.CreateAuthHeader(
_settings.ApiKey,
_settings.ClientName,
_settings.DeviceName,
_settings.DeviceId,
_settings.ClientVersion));
_logger.LogWarning("WEBSOCKET: No client auth found in headers, falling back to server API key for {DeviceId}", deviceId);
}
else
@@ -1499,7 +1499,7 @@ public class SpotifyTrackMatchingService : BackgroundService
var headers = new HeaderDictionary();
if (!string.IsNullOrEmpty(jellyfinSettings.ApiKey))
{
headers["X-Emby-Authorization"] = $"MediaBrowser Token=\"{jellyfinSettings.ApiKey}\"";
headers["Authorization"] = $"MediaBrowser Client=\"Allstarr\", Device=\"Server\", DeviceId=\"allstarr-playlists\", Version=\"{AppVersion.Version}\", Token=\"{jellyfinSettings.ApiKey}\"";
}
// Request all fields that clients typically need (not just MediaSources)
+1
View File
@@ -122,6 +122,7 @@ function patchFetchForAuthRecovery() {
if (
response.status === 401 &&
response.headers.get("X-Allstarr-Session-Expired") === "true" &&
url.includes("/api/admin") &&
!url.includes("/api/admin/auth/") &&
!authRecoveryInProgress