76 lines
2.2 KiB
Go
76 lines
2.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"database/sql"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/config"
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
)
|
|
|
|
// ExtAuth resolves users from a trusted reverse proxy's identity header,
|
|
// Navidrome-style. It never talks to an identity provider itself: the
|
|
// proxy is assumed to have already authenticated the browser user.
|
|
//
|
|
// Security-critical: the identity header is only trusted when the actual
|
|
// TCP peer address (r.RemoteAddr) matches a configured trusted CIDR.
|
|
// Headers like X-Forwarded-For/X-Real-IP/Forwarded are never consulted
|
|
// here, because they are attacker-controlled unless a dedicated trusted-
|
|
// proxy chain abstraction validates them — an arbitrary client could
|
|
// otherwise set the identity header directly and spoof any user.
|
|
type ExtAuth struct {
|
|
DB *sql.DB
|
|
Config config.ExtAuthConfig
|
|
}
|
|
|
|
// IdentityFromRequest returns the trusted username asserted by the
|
|
// reverse proxy for this request, or "" if external auth is disabled, no
|
|
// header was sent, or the connection did not originate from a trusted
|
|
// peer.
|
|
func (e *ExtAuth) IdentityFromRequest(r *http.Request) string {
|
|
if !e.Config.Enabled {
|
|
return ""
|
|
}
|
|
if !e.peerTrusted(r) {
|
|
return ""
|
|
}
|
|
return r.Header.Get(e.Config.UserHeader)
|
|
}
|
|
|
|
func (e *ExtAuth) peerTrusted(r *http.Request) bool {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host = r.RemoteAddr
|
|
}
|
|
addr, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, prefix := range e.Config.TrustedSources {
|
|
if prefix.Contains(addr) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ResolveUser returns the VibeSonic user for a trusted external identity,
|
|
// creating one if AutoCreateUsers is set and no such user exists yet. It
|
|
// never creates a second, separate "external" user type: external users
|
|
// are ordinary users with ExternalAuth set.
|
|
func (e *ExtAuth) ResolveUser(username string) (*database.User, error) {
|
|
user, err := database.GetUserByUsername(e.DB, username)
|
|
if err == nil {
|
|
return user, nil
|
|
}
|
|
if err != database.ErrNotFound {
|
|
return nil, err
|
|
}
|
|
if !e.Config.AutoCreateUsers {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
return database.CreateExternalUser(e.DB, username)
|
|
}
|