navidrome/core/apiauth/credentials.go

68 lines
1.7 KiB
Go

package apiauth
import (
"context"
"crypto/subtle"
"errors"
"github.com/navidrome/navidrome/model"
)
type Outcome int
const (
NotMine Outcome = iota
Authenticated
Rejected
Unavailable
)
type CredentialResult struct {
Outcome Outcome
User *model.User
Provider string
PasswordLocal bool
}
type CredentialChecker interface {
Check(ctx context.Context, username, password string) (CredentialResult, error)
}
// checkCredentials asks each checker in turn; only NotMine moves on, so an owning provider's "no" is final.
func checkCredentials(ctx context.Context, checkers []CredentialChecker, username, password string) (CredentialResult, error) {
for _, c := range checkers {
res, err := c.Check(ctx, username, password)
if err != nil {
return CredentialResult{}, err
}
switch res.Outcome {
case NotMine:
continue
case Authenticated:
return res, nil
case Unavailable:
return CredentialResult{}, model.ErrNotAvailable
default:
return CredentialResult{}, model.ErrInvalidAuth
}
}
return CredentialResult{}, model.ErrInvalidAuth
}
type dbChecker struct {
ds model.DataStore
}
func (c dbChecker) Check(ctx context.Context, username, password string) (CredentialResult, error) {
u, err := c.ds.User().FindByUsernameWithPassword(ctx, username)
if errors.Is(err, model.ErrNotFound) {
return CredentialResult{Outcome: NotMine}, nil
}
if err != nil {
return CredentialResult{}, err
}
if subtle.ConstantTimeCompare([]byte(u.Password), []byte(password)) != 1 {
return CredentialResult{Outcome: Rejected}, nil
}
return CredentialResult{Outcome: Authenticated, User: u, Provider: "password", PasswordLocal: true}, nil
}