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

  1. Re-authenticate via the Jellyfin auth endpoint (POST /jellyfin/Users/AuthenticateByName) to obtain a fresh AccessToken
  2. Send the token as X-Emby-Token: <token> or as Authorization: MediaBrowser Token="<token>" on every request
  3. Verify the user exists and is not disabled on this server, and that you are authenticating against the correct backend instance
  4. 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

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

Related errors


AI-assisted analysis of navidrome/navidrome@4ed7494a32 (2026-09-01). Data as JSON: /api/errors/a8bf3ac2cdedc69e. Report an issue: GitHub.