diff --git a/server/subsonic/middlewares.go b/server/subsonic/middlewares.go index 0c701484d..97fc29fe5 100644 --- a/server/subsonic/middlewares.go +++ b/server/subsonic/middlewares.go @@ -25,7 +25,6 @@ import ( "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server/subsonic/responses" - . "github.com/navidrome/navidrome/utils/gg" "github.com/navidrome/navidrome/utils/req" ) @@ -95,54 +94,64 @@ func checkRequiredParameters(next http.Handler) http.Handler { }) } +// authenticateRequest validates the authentication credentials in an HTTP request and returns +// the authenticated user. It supports internal auth, reverse proxy auth, and Subsonic classic +// auth (username + password/token/salt/jwt query params). +// +// Callers should handle specific error types as needed: +// - context.Canceled: request was canceled during authentication +// - model.ErrNotFound: username not found in database +// - model.ErrInvalidAuth: invalid credentials (wrong password, token, etc.) +func authenticateRequest(ds model.DataStore, r *http.Request) (*model.User, error) { + ctx := r.Context() + + // Check internal auth or reverse proxy auth first + username, _ := fromInternalOrProxyAuth(r) + if username != "" { + return ds.User(ctx).FindByUsername(username) + } + + // Fall back to Subsonic classic auth (query params) + p := req.Params(r) + username, _ = p.String("u") + if username == "" { + return nil, model.ErrInvalidAuth + } + + pass, _ := p.String("p") + token, _ := p.String("t") + salt, _ := p.String("s") + jwt, _ := p.String("jwt") + + usr, err := ds.User(ctx).FindByUsernameWithPassword(username) + if err != nil { + return nil, err + } + + if err := validateCredentials(usr, pass, token, salt, jwt); err != nil { + return nil, err + } + + return usr, nil +} + func authenticate(ds model.DataStore) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - var usr *model.User - var err error - - username, isInternalAuth := fromInternalOrProxyAuth(r) - if username != "" { - authType := If(isInternalAuth, "internal", "reverse-proxy") - usr, err = ds.User(ctx).FindByUsername(username) - if errors.Is(err, context.Canceled) { - log.Debug(ctx, "API: Request canceled when authenticating", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err) - return - } - if errors.Is(err, model.ErrNotFound) { - log.Warn(ctx, "API: Invalid login", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err) - } else if err != nil { - log.Error(ctx, "API: Error authenticating username", "auth", authType, "username", username, "remoteAddr", r.RemoteAddr, err) - } - } else { - p := req.Params(r) - username, _ := p.String("u") - pass, _ := p.String("p") - token, _ := p.String("t") - salt, _ := p.String("s") - jwt, _ := p.String("jwt") - - usr, err = ds.User(ctx).FindByUsernameWithPassword(username) - if errors.Is(err, context.Canceled) { - log.Debug(ctx, "API: Request canceled when authenticating", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err) - return - } - switch { - case errors.Is(err, model.ErrNotFound): - log.Warn(ctx, "API: Invalid login", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err) - case err != nil: - log.Error(ctx, "API: Error authenticating username", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err) - default: - err = validateCredentials(usr, pass, token, salt, jwt) - if err != nil { - log.Warn(ctx, "API: Invalid login", "auth", "subsonic", "username", username, "remoteAddr", r.RemoteAddr, err) - } - } - } - + usr, err := authenticateRequest(ds, r) if err != nil { + username, _ := request.UsernameFrom(ctx) + switch { + case errors.Is(err, context.Canceled): + log.Debug(ctx, "API: Request canceled when authenticating", "username", username, "remoteAddr", r.RemoteAddr, err) + return + case errors.Is(err, model.ErrNotFound), errors.Is(err, model.ErrInvalidAuth): + log.Warn(ctx, "API: Invalid login", "username", username, "remoteAddr", r.RemoteAddr, err) + default: + log.Error(ctx, "API: Error authenticating", "username", username, "remoteAddr", r.RemoteAddr, err) + } sendError(w, r, newError(responses.ErrorAuthenticationFail)) return } @@ -157,13 +166,9 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler { // Unlike the authenticate middleware, this function does not write any HTTP response, making it suitable // for use by external consumers (e.g., plugin endpoints) that need Subsonic auth but want to handle // errors themselves. -// -// It supports the same authentication methods as the Subsonic API: -// - Internal auth (from plugin SubsonicAPI calls) -// - Reverse proxy auth (via trusted external auth headers) -// - Subsonic classic auth (username + password/token/salt/jwt query params) func ValidateAuth(ds model.DataStore, r *http.Request) (*model.User, error) { - // Parse form data into query params (same as postFormToQueryParams middleware) + // Parse form data into query params (same as postFormToQueryParams middleware, + // which is not in the call chain when ValidateAuth is used directly) if err := r.ParseForm(); err != nil { return nil, fmt.Errorf("parsing form: %w", err) } @@ -175,42 +180,7 @@ func ValidateAuth(ds model.DataStore, r *http.Request) (*model.User, error) { } r.URL.RawQuery = strings.Join(parts, "&") - ctx := r.Context() - - // Check internal auth or reverse proxy auth first - username, isInternalAuth := fromInternalOrProxyAuth(r) - if username != "" { - usr, err := ds.User(ctx).FindByUsername(username) - if err != nil { - authType := If(isInternalAuth, "internal", "reverse-proxy") - log.Warn(ctx, "Plugin auth: Invalid login", "auth", authType, "username", username, err) - return nil, model.ErrInvalidAuth - } - return usr, nil - } - - // Fall back to Subsonic classic auth (query params) - p := req.Params(r) - username, _ = p.String("u") - if username == "" { - return nil, fmt.Errorf("missing required parameter 'u' (username)") - } - - pass, _ := p.String("p") - token, _ := p.String("t") - salt, _ := p.String("s") - jwt, _ := p.String("jwt") - - usr, err := ds.User(ctx).FindByUsernameWithPassword(username) - if err != nil { - return nil, model.ErrInvalidAuth - } - - if err := validateCredentials(usr, pass, token, salt, jwt); err != nil { - return nil, err - } - - return usr, nil + return authenticateRequest(ds, r) } func validateCredentials(user *model.User, pass, token, salt, jwt string) error {