navidrome/navidrome · critical
Unauthorized
Error message
Unauthorized
What it means
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.
Source
Thrown at server/jellyfin/middlewares.go:181
return model.User{}, false
}
usr, err := api.ds.User(r.Context()).FindByUsername(claims.Subject)
if err != nil {
log.Warn(r.Context(), "Jellyfin API: token subject not found", "user", claims.Subject, err)
return model.User{}, false
}
if err := auth.CheckClaims(claims, *usr, auth.AudienceJellyfin); err != nil {
log.Warn(r.Context(), "Jellyfin API: rejected token", "user", claims.Subject, err)
return model.User{}, false
}
return *usr, true
}
func (api *Router) authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
usr, ok := api.userFromToken(r)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := request.WithUser(r.Context(), usr)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// withPlayer resolves/registers a model.Player for the calling device into the context, mirroring
// Subsonic's getPlayer. Jellyfin clients always send a DeviceId in the auth header (unlike Subsonic),
// so it's used directly as the player id and reports from the same install share a player/scrobbling
// session.
func (api *Router) withPlayer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if api.players == nil { // fail open when players isn't wired (e.g. in unit tests)
next.ServeHTTP(w, r)
return
}
ctx := r.Context()View on GitHub (pinned to 4ed7494a32)
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)
Example fix
// before: wrong header scheme -> 401
req.Header.Set("Authorization", "Bearer " + token)
// after: Jellyfin-compatible header
req.Header.Set("X-Emby-Token", token)
// or: req.Header.Set("Authorization", "MediaBrowser Token=\""+token+"\"") Defensive patterns
Strategy: try-catch
Validate before calling
func ensureAuth(client *Client) error {
if client.Token == "" {
return client.AuthenticateByName(client.User, client.Password) // sets AccessToken
}
return nil
} Try / catch
resp, err := client.Do(req)
if err == nil && resp.StatusCode == http.StatusUnauthorized {
if rerr := client.AuthenticateByName(user, pass); rerr != nil {
return rerr // credentials truly invalid
}
req.Header.Set("X-Emby-Token", client.AccessToken)
return client.Do(req) // retry once with fresh token
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- ErrInvalidAuth
- deezer: failed to parse auth response: %w
- ListenBrainz: HTTP Error, Status: (%d)
- Bad Request
- invalid image file
AI-assisted analysis of navidrome/navidrome@4ed7494a32 (2026-09-01).
Data as JSON: /api/errors/a8bf3ac2cdedc69e.
Report an issue: GitHub.