Skip to content

fix(jellyfin): support Jellyfin 12 authorization - #128

Merged
retardgerman merged 1 commit into
devfrom
fix/jellyfin-12-auth
Sep 11, 2026
Merged

retardgerman merged 1 commit into
devfrom
fix/jellyfin-12-auth

Conversation

@retardgerman

Copy link
Copy Markdown
Contributor

Problem

Jellyfin 12.0 disables legacy authorization by default, and a migration turns it off on existing installs as well. AuthorizationContext.cs now gates the old paths behind EnableLegacyAuthorization:

  • X-MediaBrowser-Token header — gated, no longer read
  • api_key query parameter — gated, no longer read
  • Authorization: MediaBrowser … header — always read
  • ApiKey query parameter — always read

Anchorr used the first two exclusively, so every Jellyfin request returned 401 after upgrading. Reported as:

error: Request failed with status code 401
    at async routes/jellyfinRoutes.js:87:22

The WebSocket handshake goes through the same AuthorizationContext (WebSocketManager.WebSocketRequestHandler calls _authService.Authenticate), so it was affected too.

Fix

  • New shared jellyfinAuthHeaders() helper in api/jellyfin.js emitting Authorization: MediaBrowser Client="Anchorr", …, Token="…". All 11 call sites converted, plus the one in routes/jellyfinRoutes.js. The poller, webhook, seeder, pruner, resolver and weekly roundup all route through api/jellyfin.js and inherit the change.
  • WebSocket handshake moved from ?api_key= to ?ApiKey=, with the key URL-encoded. An unencoded key containing & or # could previously truncate or append query parameters.
  • Both forms are accepted by 10.10.7 and 12.0, so no version switch is required and older servers keep working.

Hardening picked up while reviewing the change:

  • The helper rejects a missing or malformed API key rather than emitting a well-formed header carrying an empty or mangled token. Silently stripping illegal characters would send a different token and surface as an indistinguishable 401. The key is trimmed first, so a value pasted with a trailing newline still works.
  • WebSocket auth rejections (401/403 handshake, close code 1008) now log the actual cause instead of looping on reconnect with a generic transport error.
  • Raw axios errors are no longer passed to the logger on the Jellyfin paths. The serialized error includes config.headers, which wrote the Authorization header to logs/ in cleartext. This predates the change (it leaked X-MediaBrowser-Token the same way) but is fixed here since this PR introduces the new header. Affected: routes/jellyfinRoutes.js (2x), jellyfinPoller.js.

Verification

  • Outgoing request captured against a local HTTP server: the Authorization header is emitted correctly and no X-MediaBrowser-Token is sent.
  • Helper edge cases exercised: empty, undefined, whitespace-padded, quote, comma, backslash, NUL, tab, DEL, CRLF.
  • Confirmed a token no longer reaches logs/ after the logger change.

Not tested against a live Jellyfin 12 instance.

Out of scope

  • The reconnect loop still retries indefinitely on auth failures; it only logs the cause now.
  • The silent CollectionId fallback in fetchLibraries can route items to the wrong Discord channel when Jellyfin rejects /Items. Pre-existing, worth its own issue.
  • The logger.error("...", err) pattern exists at roughly 20 other call sites. Only the Jellyfin paths were touched, since only those carry an auth header.

Jellyfin 12.0 disables legacy authorization by default and a migration turns
it off on existing installs too, so the X-MediaBrowser-Token header and the
api_key query parameter no longer authenticate. Every Jellyfin request came
back 401 after the upgrade.

Switch to the standard Authorization header with the MediaBrowser scheme via
a shared jellyfinAuthHeaders() helper, and move the WebSocket handshake to
the ApiKey query parameter. Both are accepted by 10.10.x and 12.x, so no
version switch is needed.

The helper rejects a missing or malformed API key instead of sending a
well-formed header with a wrong token, which would only surface as a generic
401. WebSocket auth rejections now log the actual reason rather than looping
silently on reconnect.

Also stop passing raw axios errors to the logger on the Jellyfin paths: the
serialized error carries config.headers, which would write the Authorization
header to disk in cleartext.
@retardgerman
retardgerman merged commit 14095b3 into dev Sep 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant