diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..21058f1 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,21 @@ +package auth + +import ( + "context" + "fmt" + + "git.sr.ht/~emersion/soju/database" +) + +type PlainAuthenticator interface { + AuthPlain(ctx context.Context, db database.Database, username, password string) error +} + +func New(driver, source string) (PlainAuthenticator, error) { + switch driver { + case "internal": + return NewInternal(), nil + default: + return nil, fmt.Errorf("unknown auth driver %q", driver) + } +} diff --git a/auth/internal.go b/auth/internal.go new file mode 100644 index 0000000..509803c --- /dev/null +++ b/auth/internal.go @@ -0,0 +1,34 @@ +package auth + +import ( + "context" + "fmt" + + "git.sr.ht/~emersion/soju/database" +) + +type internal struct{} + +func NewInternal() PlainAuthenticator { + return internal{} +} + +func (internal) AuthPlain(ctx context.Context, db database.Database, username, password string) error { + u, err := db.GetUser(ctx, username) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + upgraded, err := u.CheckPassword(password) + if err != nil { + return err + } + + if upgraded { + if err := db.StoreUser(ctx, u); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/soju/main.go b/cmd/soju/main.go index 2a0e8c5..b2999d7 100644 --- a/cmd/soju/main.go +++ b/cmd/soju/main.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "git.sr.ht/~emersion/soju" + "git.sr.ht/~emersion/soju/auth" "git.sr.ht/~emersion/soju/config" "git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/identd" @@ -75,6 +76,11 @@ func loadConfig() (*config.Server, *soju.Config, error) { motd = strings.TrimSuffix(string(b), "\n") } + auth, err := auth.New(raw.Auth.Driver, raw.Auth.Source) + if err != nil { + return nil, nil, fmt.Errorf("failed to create authenticator: %v", err) + } + if raw.TLS != nil { cert, err := tls.LoadX509KeyPair(raw.TLS.CertPath, raw.TLS.KeyPath) if err != nil { @@ -94,6 +100,7 @@ func loadConfig() (*config.Server, *soju.Config, error) { DisableInactiveUsersDelay: raw.DisableInactiveUsersDelay, EnableUsersOnAuth: raw.EnableUsersOnAuth, MOTD: motd, + Auth: auth, } return raw, cfg, nil } diff --git a/config/config.go b/config/config.go index 1a37cf4..585d189 100644 --- a/config/config.go +++ b/config/config.go @@ -58,6 +58,10 @@ type MsgStore struct { Driver, Source string } +type Auth struct { + Driver, Source string +} + type Server struct { Listen []string TLS *TLS @@ -67,6 +71,7 @@ type Server struct { DB DB MsgStore MsgStore + Auth Auth HTTPOrigins []string AcceptProxyIPs IPSet @@ -91,6 +96,9 @@ func Defaults() *Server { MsgStore: MsgStore{ Driver: "memory", }, + Auth: Auth{ + Driver: "internal", + }, MaxUserNetworks: -1, } } @@ -149,6 +157,16 @@ func parse(cfg scfg.Block) (*Server, error) { default: return nil, fmt.Errorf("directive %q: unknown driver %q", d.Name, srv.MsgStore.Driver) } + case "auth": + if err := d.ParseParams(&srv.Auth.Driver); err != nil { + return nil, err + } + switch srv.Auth.Driver { + case "internal": + srv.Auth.Source = "" + default: + return nil, fmt.Errorf("directive %q: unknown driver %q", d.Name, srv.Auth.Driver) + } case "http-origin": srv.HTTPOrigins = d.Params case "accept-proxy-ip": diff --git a/doc/soju.1.scd b/doc/soju.1.scd index 1aba97d..ba67522 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -185,6 +185,14 @@ The following directives are supported: This can be used together with _disable-inactive-user_ to seamlessly disable and re-enable users during lengthy inactivity. +*auth* ... + Set the authentication method. By default, internal authentication is used. + + Supported drivers: + + *auth internal* + Use internal authentication. + # IRC SERVICE soju exposes an IRC service called *BouncerServ* to manage the bouncer. diff --git a/downstream.go b/downstream.go index b8679c0..579b056 100644 --- a/downstream.go +++ b/downstream.go @@ -1192,22 +1192,10 @@ func unmarshalUsername(rawUsername string) (username, client, network string) { func (dc *downstreamConn) authenticate(ctx context.Context, username, password string) error { username, clientName, networkName := unmarshalUsername(username) - u, err := dc.srv.db.GetUser(ctx, username) - if err != nil { - return newInvalidUsernameOrPasswordError(fmt.Errorf("user not found: %w", err)) - } - - upgraded, err := u.CheckPassword(password) - if err != nil { + if err := dc.srv.Config().Auth.AuthPlain(ctx, dc.srv.db, username, password); err != nil { return newInvalidUsernameOrPasswordError(err) } - if upgraded { - if err := dc.srv.db.StoreUser(ctx, u); err != nil { - return err - } - } - dc.user = dc.srv.getUser(username) if dc.user == nil { return fmt.Errorf("user exists in the DB but hasn't been loaded by the bouncer -- a restart may help") diff --git a/server.go b/server.go index ee3b232..03b0489 100644 --- a/server.go +++ b/server.go @@ -20,6 +20,7 @@ import ( "gopkg.in/irc.v4" "nhooyr.io/websocket" + "git.sr.ht/~emersion/soju/auth" "git.sr.ht/~emersion/soju/config" "git.sr.ht/~emersion/soju/database" "git.sr.ht/~emersion/soju/identd" @@ -143,6 +144,7 @@ type Config struct { UpstreamUserIPs []*net.IPNet DisableInactiveUsersDelay time.Duration EnableUsersOnAuth bool + Auth auth.PlainAuthenticator } type Server struct { @@ -186,6 +188,7 @@ func NewServer(db database.Database) *Server { srv.config.Store(&Config{ Hostname: "localhost", MaxUserNetworks: -1, + Auth: auth.NewInternal(), }) return srv }