mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
refactor(auth): simplify authentication logic and improve error handling
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
b0f91715b9
commit
5c52bbb130
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user