refactor(auth): simplify authentication logic and improve error handling

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-02-13 12:57:30 -05:00
parent b0f91715b9
commit 5c52bbb130

View File

@ -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 {