{"record":{"id":"a8bf3ac2cdedc69e","repo":"navidrome/navidrome","slug":"unauthorized-a8bf3a","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"http","errorClass":null,"httpStatus":401,"severity":"critical","filePath":"server/jellyfin/middlewares.go","lineNumber":181,"sourceCode":"\t\treturn model.User{}, false\n\t}\n\tusr, err := api.ds.User(r.Context()).FindByUsername(claims.Subject)\n\tif err != nil {\n\t\tlog.Warn(r.Context(), \"Jellyfin API: token subject not found\", \"user\", claims.Subject, err)\n\t\treturn model.User{}, false\n\t}\n\tif err := auth.CheckClaims(claims, *usr, auth.AudienceJellyfin); err != nil {\n\t\tlog.Warn(r.Context(), \"Jellyfin API: rejected token\", \"user\", claims.Subject, err)\n\t\treturn model.User{}, false\n\t}\n\treturn *usr, true\n}\n\nfunc (api *Router) authenticate(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tusr, ok := api.userFromToken(r)\n\t\tif !ok {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tctx := request.WithUser(r.Context(), usr)\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t})\n}\n\n// withPlayer resolves/registers a model.Player for the calling device into the context, mirroring\n// Subsonic's getPlayer. Jellyfin clients always send a DeviceId in the auth header (unlike Subsonic),\n// so it's used directly as the player id and reports from the same install share a player/scrobbling\n// session.\nfunc (api *Router) withPlayer(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif api.players == nil { // fail open when players isn't wired (e.g. in unit tests)\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\t\tctx := r.Context()","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/navidrome/navidrome/blob/4ed7494a3293a9e9e647897ebfb9be327efd981b/server/jellyfin/middlewares.go#L163-L199","documentation":"The authenticate middleware (server/jellyfin/middlewares.go) guards every Jellyfin-compatible route: it calls api.userFromToken(r) to resolve the request's authorization token to a user, and when that fails it writes 401 'Unauthorized' and stops the chain. This means the request had no valid X-Emby-Token / X-MediaBrowser-Token header, no Authorization scheme the parser understands, an expired or revoked token, or a token belonging to a disabled/deleted user. Nothing about the request body or path matters — auth is checked first.","triggerScenarios":"Calling any /jellyfin endpoint without an auth token header; using a token generated before a server re-install or user deletion; token copied with extra whitespace or wrong header name (e.g. Bearer where the server expects X-Emby-Token, or vice versa); clients that authenticated against a different backend (real Jellyfin vs this server).","commonSituations":"Subsonic-only users trying the Jellyfin API without enabling/creating a Jellyfin-style token; scripts hardcoding an Authorization header format from a different app; reverse proxy stripping custom X- headers; password change or token rotation invalidating cached credentials in clients like Symfonium/Finch.","solutions":["Re-authenticate via the Jellyfin auth endpoint (POST /jellyfin/Users/AuthenticateByName) to obtain a fresh AccessToken","Send the token as X-Emby-Token: <token> or as Authorization: MediaBrowser Token=\"<token>\" on every request","Verify the user exists and is not disabled on this server, and that you are authenticating against the correct backend instance","Check that no reverse proxy or middleware strips the custom auth headers (X-Emby-Token, X-MediaBrowser-Token)"],"exampleFix":"// before: wrong header scheme -> 401\nreq.Header.Set(\"Authorization\", \"Bearer \" + token)\n// after: Jellyfin-compatible header\nreq.Header.Set(\"X-Emby-Token\", token)\n// or: req.Header.Set(\"Authorization\", \"MediaBrowser Token=\\\"\"+token+\"\\\"\")","handlingStrategy":"try-catch","validationCode":"func ensureAuth(client *Client) error {\n    if client.Token == \"\" {\n        return client.AuthenticateByName(client.User, client.Password) // sets AccessToken\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"resp, err := client.Do(req)\nif err == nil && resp.StatusCode == http.StatusUnauthorized {\n    if rerr := client.AuthenticateByName(user, pass); rerr != nil {\n        return rerr // credentials truly invalid\n    }\n    req.Header.Set(\"X-Emby-Token\", client.AccessToken)\n    return client.Do(req) // retry once with fresh token\n}","preventionTips":["Attach X-Emby-Token (or MediaBrowser Authorization scheme) to every request","Re-authenticate on 401 instead of retrying with the same token","Refresh stored tokens after server reinstalls, password changes, or user deletion","Verify proxies do not strip X-Emby-Token/X-MediaBrowser-Token headers"],"tags":["http-401","jellyfin","authentication","middleware"],"backgroundTag":"missing-or-invalid-auth-token","analyzedSha":"4ed7494a3293a9e9e647897ebfb9be327efd981b","analyzedAt":"2026-09-01T05:03:05.018Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}