diff --git a/go.mod b/go.mod index 0e7c8de9386d2a8e75aaa500b1be10a23ac0a39a..3850998540fc1952c867b92a5400cf1f318c2765 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( git.autistici.org/ai3/go-common v0.0.0-20210109162342-daf4a31c8d05 git.autistici.org/ai3/tools/webappdb v0.0.0-20200630154205-9f45389dbd94 - git.autistici.org/id/auth v0.0.0-20200212081728-3d44524ae2e5 + git.autistici.org/id/auth v0.0.0-20210109163523-998aa7f16d74 git.autistici.org/id/go-sso v0.0.0-20190630084805-a218499d322a git.autistici.org/id/usermetadb v0.0.0-20200209112823-95a30f3b610e github.com/boombuler/barcode v0.0.0-20170618053812-56ef0af91246 // indirect diff --git a/go.sum b/go.sum index 3997cae25961235add1b3b1062c78d9c50e5f7d1..07f15e7eac7700891e8a5201baf775c7da51c0ed 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ git.autistici.org/ai3/tools/webappdb v0.0.0-20200630154205-9f45389dbd94 h1:8thNL git.autistici.org/ai3/tools/webappdb v0.0.0-20200630154205-9f45389dbd94/go.mod h1:jqgGzT98QtJE8XFeV1zZKp7ZA1m/hFz/8zABI+x9u6o= git.autistici.org/id/auth v0.0.0-20200212081728-3d44524ae2e5 h1:mxuJFOy4mgSJd54eBC2nVvT4mv9t8qp14mAin+TBnP0= git.autistici.org/id/auth v0.0.0-20200212081728-3d44524ae2e5/go.mod h1:opFyv0ktv8UuXHezBQL3FrUg6en8h8P5I14kLMBC1Jg= +git.autistici.org/id/auth v0.0.0-20210109163523-998aa7f16d74 h1:uW+AZHkw2FfWTxjmQrZJW7oLqT45XqSnfQVtZhKEq1Y= +git.autistici.org/id/auth v0.0.0-20210109163523-998aa7f16d74/go.mod h1:opFyv0ktv8UuXHezBQL3FrUg6en8h8P5I14kLMBC1Jg= git.autistici.org/id/go-sso v0.0.0-20190630084805-a218499d322a h1:O8c4NsokNX1kV7oP5n900pec7ripj5/zatHh3fVzEhg= git.autistici.org/id/go-sso v0.0.0-20190630084805-a218499d322a/go.mod h1:B9omXX7rw0qgWdBoF4RZnM7clwEVejoAe8oNJWETBZ0= git.autistici.org/id/usermetadb v0.0.0-20200209112823-95a30f3b610e h1:GUm8esgUGh093YR6N8DvMUUo6aFjswd2ot1oCfPsnjw= diff --git a/vendor/git.autistici.org/id/auth/README.md b/vendor/git.autistici.org/id/auth/README.md index b01b20ab50dc66bb2e6b890149cb65d756c18f47..8c770dedfd6bf52915e09f0c4af9ee51f6e7366e 100644 --- a/vendor/git.autistici.org/id/auth/README.md +++ b/vendor/git.autistici.org/id/auth/README.md @@ -21,10 +21,11 @@ backends such as Memcached for short-term storage, and anonymized user activity data. For this reason, it is recommended to install an auth-server on every host. -It listens for authorization requests over a UNIX socket. UNIX -permissions should be used to control access to the socket if -necessary. Clients speak a custom simple line-based attribute/value -protocol, and can send multiple requests over the same connection. +The authentication protocol is a simple line-based text protocol. The +auth-server can listen on a UNIX or TCP socket: in the first case, +filesystem permissions should be used to control access to the socket, +while in the second case there is support for SSL, with optional +checks on the provided client certificates. ## Services @@ -37,8 +38,9 @@ functionality and user backends. The authentication server data model is based on the concept of a *user account*. The server knows how to retrieve user accounts stored -in LDAP, but it has to be told the specific details of how to find -them and how to map the information there to what it needs. +in LDAP or SQL databases, but it has to be told the specific details +of how to find them and how to map the information there to what it +needs. ## Other Dependencies @@ -306,6 +308,9 @@ substitution symbol `?` as placeholder for query parameters. The only mandatory query is *get_user*, if the other ones are not specified the associated fields will be empty. +Note that the relational queries (*get_user_groups*, *get_user_u2f* +and *get_user_asp*) should NOT return rows containing NULL values. + ### Example database schema The following could be a (very simple) example database schema for a @@ -367,6 +372,9 @@ add specific users to it easily. The daemon can run either standalone or be socket-activated by systemd, which is what the Debian package does. +Check out the output of *auth-server --help* for documentation on how +to configure the listening sockets. + ## Wire protocol The rationale behind the wire protocol ("why not http?") is twofold: diff --git a/vendor/git.autistici.org/id/auth/backend/backend.go b/vendor/git.autistici.org/id/auth/backend/backend.go index 22497d78c85a14175a19689275c997fe549c6302..f2cd8072f2b78137dfc3d3c14f21cdc5b0ccc49f 100644 --- a/vendor/git.autistici.org/id/auth/backend/backend.go +++ b/vendor/git.autistici.org/id/auth/backend/backend.go @@ -29,11 +29,21 @@ type AppSpecificPassword struct { EncryptedPassword []byte } -// Has2FA returns true if the user supports any 2FA method. +// Has2FA returns true if the user supports any interactive 2FA method. func (u *User) Has2FA() bool { return u.HasU2F() || u.HasOTP() } +// HasASPs returns true if the user has app-specific passwords. +func (u *User) HasASPs(service string) bool { + for _, asp := range u.AppSpecificPasswords { + if asp.Service == service { + return true + } + } + return false +} + // HasOTP returns true if the user supports (T)OTP. func (u *User) HasOTP() bool { return u.TOTPSecret != "" diff --git a/vendor/git.autistici.org/id/auth/backend/file/file.go b/vendor/git.autistici.org/id/auth/backend/file/file.go index 46ac5e40276469a72508fb8a8f8deb8684e129e0..0cd1ff28aab0917cd1159334626e4016094d2aaf 100644 --- a/vendor/git.autistici.org/id/auth/backend/file/file.go +++ b/vendor/git.autistici.org/id/auth/backend/file/file.go @@ -34,6 +34,11 @@ type fileUser struct { KeyHandle string `yaml:"key_handle"` PublicKey string `yaml:"public_key"` } `yaml:"u2f_registrations"` + + AppSpecificPasswords []struct { + Service string `yaml:"service"` + EncryptedPassword string `yaml:"password"` + } `yaml:"app_specific_passwords"` } func (f *fileUser) getU2FRegistrations(filename string) []u2f.Registration { @@ -64,7 +69,7 @@ func (f *fileUser) getU2FRegistrations(filename string) []u2f.Registration { } func (f *fileUser) toUser(filename string) *backend.User { - return &backend.User{ + u := &backend.User{ Name: f.Name, Email: f.Email, Shard: f.Shard, @@ -73,6 +78,13 @@ func (f *fileUser) toUser(filename string) *backend.User { Groups: f.Groups, U2FRegistrations: f.getU2FRegistrations(filename), } + for _, asp := range f.AppSpecificPasswords { + u.AppSpecificPasswords = append(u.AppSpecificPasswords, &backend.AppSpecificPassword{ + Service: asp.Service, + EncryptedPassword: []byte(asp.EncryptedPassword), + }) + } + return u } // Simple file-based authentication backend, list users and their diff --git a/vendor/git.autistici.org/id/auth/backend/ldap/ldap.go b/vendor/git.autistici.org/id/auth/backend/ldap/ldap.go index ab4c6796912f4a029b560c3eb6be231f131df5de..bcf47a6e70028daa049c23cac673074c6879526a 100644 --- a/vendor/git.autistici.org/id/auth/backend/ldap/ldap.go +++ b/vendor/git.autistici.org/id/auth/backend/ldap/ldap.go @@ -10,7 +10,7 @@ import ( ldaputil "git.autistici.org/ai3/go-common/ldap" ct "git.autistici.org/ai3/go-common/ldap/compositetypes" "github.com/tstranex/u2f" - "gopkg.in/ldap.v3" + "github.com/go-ldap/ldap/v3" "gopkg.in/yaml.v2" "git.autistici.org/id/auth/backend" diff --git a/vendor/git.autistici.org/id/auth/backend/sql/sql.go b/vendor/git.autistici.org/id/auth/backend/sql/sql.go index f8196f173e29b5fdd54b2718b7331ae152b1035d..24b2de9f5ab472236f1f26756e7fba5d092e0035 100644 --- a/vendor/git.autistici.org/id/auth/backend/sql/sql.go +++ b/vendor/git.autistici.org/id/auth/backend/sql/sql.go @@ -124,7 +124,9 @@ func (b *sqlServiceBackend) GetUser(ctx context.Context, name string) (*backend. // Use NullStrings for optional fields. var nullableTOTP, nullableShard sql.NullString - row := tx.Stmt(b.stmts[sqlQueryGetUser]).QueryRow(name) + stmt := tx.Stmt(b.stmts[sqlQueryGetUser]) + defer stmt.Close() + row := stmt.QueryRow(name) if err := row.Scan(&user.Email, &user.EncryptedPassword, &nullableTOTP, &nullableShard); err != nil { return nil, false } @@ -150,11 +152,13 @@ func (b *sqlServiceBackend) GetUser(ctx context.Context, name string) (*backend. } func (b *sqlServiceBackend) getUserU2FRegistrations(tx *sql.Tx, name string) ([]u2f.Registration, error) { - stmt, ok := b.stmts[sqlQueryGetU2F] + stmtTpl, ok := b.stmts[sqlQueryGetU2F] if !ok { return nil, nil } - rows, err := tx.Stmt(stmt).Query(name) + stmt := tx.Stmt(stmtTpl) + defer stmt.Close() + rows, err := stmt.Query(name) if err != nil { return nil, err } @@ -175,15 +179,17 @@ func (b *sqlServiceBackend) getUserU2FRegistrations(tx *sql.Tx, name string) ([] } out = append(out, *reg) } - return out, nil + return out, rows.Err() } func (b *sqlServiceBackend) getUserASPs(tx *sql.Tx, name string) ([]*backend.AppSpecificPassword, error) { - stmt, ok := b.stmts[sqlQueryGetASP] + stmtTpl, ok := b.stmts[sqlQueryGetASP] if !ok { return nil, nil } - rows, err := tx.Stmt(stmt).Query(name) + stmt := tx.Stmt(stmtTpl) + defer stmt.Close() + rows, err := stmt.Query(name) if err != nil { return nil, err } @@ -197,15 +203,17 @@ func (b *sqlServiceBackend) getUserASPs(tx *sql.Tx, name string) ([]*backend.App } out = append(out, &asp) } - return out, nil + return out, rows.Err() } func (b *sqlServiceBackend) getUserGroups(tx *sql.Tx, name string) ([]string, error) { - stmt, ok := b.stmts[sqlQueryGetGroups] + stmtTpl, ok := b.stmts[sqlQueryGetGroups] if !ok { return nil, nil } - rows, err := tx.Stmt(stmt).Query(name) + stmt := tx.Stmt(stmtTpl) + defer stmt.Close() + rows, err := stmt.Query(name) if err != nil { return nil, err } @@ -219,6 +227,5 @@ func (b *sqlServiceBackend) getUserGroups(tx *sql.Tx, name string) ([]string, er } out = append(out, group) } - return out, nil - + return out, rows.Err() } diff --git a/vendor/git.autistici.org/id/auth/client/client.go b/vendor/git.autistici.org/id/auth/client/client.go index 2bb601853692ab7c021a84b16819ce3baef324c3..8c81c7a9c142c522ce7bb90be5a65aef67645783 100644 --- a/vendor/git.autistici.org/id/auth/client/client.go +++ b/vendor/git.autistici.org/id/auth/client/client.go @@ -3,15 +3,19 @@ package client import ( "context" "net" - "net/textproto" + "strings" "github.com/cenkalti/backoff" "go.opencensus.io/trace" "git.autistici.org/id/auth" + "git.autistici.org/id/auth/lineproto" ) -var DefaultSocketPath = "/run/auth/socket" +var ( + DefaultSocketPath = "/run/auth/socket" + DefaultPoolSize = 3 +) type Client interface { Authenticate(context.Context, *auth.Request) (*auth.Response, error) @@ -20,12 +24,20 @@ type Client interface { type socketClient struct { socketPath string codec auth.Codec + pool *Pool } func New(socketPath string) Client { return &socketClient{ socketPath: socketPath, codec: auth.DefaultCodec, + pool: NewPool(func() (*lineproto.Conn, error) { + c, err := net.Dial("unix", socketPath) + if err != nil { + return nil, err + } + return lineproto.NewConn(c, ""), nil + }, DefaultPoolSize), } } @@ -47,6 +59,8 @@ func (c *socketClient) Authenticate(ctx context.Context, req *auth.Request) (*au resp, err = c.doAuthenticate(sctx, req) if err == nil { return nil + } else if strings.Contains(err.Error(), "use of closed network connection") { + return err } else if netErr, ok := err.(net.Error); ok && netErr.Temporary() { return netErr } @@ -59,52 +73,45 @@ func (c *socketClient) Authenticate(ctx context.Context, req *auth.Request) (*au } func (c *socketClient) doAuthenticate(ctx context.Context, req *auth.Request) (*auth.Response, error) { - // Create the connection outside of the timed goroutine, so - // that we can call Close() on exit regardless of the reason: - // this way, when a timeout occurs or the context is canceled, - // the pending request terminates immediately. - conn, err := textproto.Dial("unix", c.socketPath) - if err != nil { - return nil, err - } - defer conn.Close() - - // Make space in the channel for at least one element, or we - // will leak a goroutine whenever the authentication request - // times out. - done := make(chan error, 1) var resp auth.Response - - go func() { - defer close(done) - - // Write the auth command to the connection. - if err := conn.PrintfLine("auth %s", string(c.codec.Encode(req))); err != nil { - done <- err - return + err := c.pool.WithConn(func(conn *lineproto.Conn) error { + // Make space in the channel for at least one element, or we + // will leak a goroutine whenever the authentication request + // times out. + done := make(chan error, 1) + + go func() { + defer close(done) + + // Write the auth command to the connection. + if err := conn.WriteLine([]byte("auth "), c.codec.Encode(req)); err != nil { + done <- err + return + } + + // Read the response. + line, err := conn.ReadLine() + if err != nil { + done <- err + return + } + if err := c.codec.Decode(line, &resp); err != nil { + done <- err + return + } + done <- nil + }() + + // Wait for the call to terminate, or the context to time out, + // whichever happens first. + select { + case err := <-done: + return err + case <-ctx.Done(): + return ctx.Err() } - - // Read the response. - line, err := conn.ReadLineBytes() - if err != nil { - done <- err - return - } - if err := c.codec.Decode(line, &resp); err != nil { - done <- err - return - } - done <- nil - }() - - // Wait for the call to terminate, or the context to time out, - // whichever happens first. - select { - case err := <-done: - return &resp, err - case <-ctx.Done(): - return nil, ctx.Err() - } + }) + return &resp, err } func responseToTraceStatus(resp *auth.Response, err error) trace.Status { diff --git a/vendor/git.autistici.org/id/auth/client/pool.go b/vendor/git.autistici.org/id/auth/client/pool.go new file mode 100644 index 0000000000000000000000000000000000000000..f4abf1f5b0276e9f5c931436d9ec0b935b121366 --- /dev/null +++ b/vendor/git.autistici.org/id/auth/client/pool.go @@ -0,0 +1,47 @@ +package client + +import ( + "git.autistici.org/id/auth/lineproto" +) + +type PoolDialer func() (*lineproto.Conn, error) + +type Pool struct { + ch chan *lineproto.Conn + dialer PoolDialer +} + +func NewPool(dialer PoolDialer, size int) *Pool { + return &Pool{ + ch: make(chan *lineproto.Conn, size), + dialer: dialer, + } +} + +func (p *Pool) WithConn(f func(*lineproto.Conn) error) error { + // Acquire a connection. + var conn *lineproto.Conn + select { + case conn = <-p.ch: + default: + var err error + conn, err = p.dialer() + if err != nil { + return err + } + } + + // Run the function and inspect its return value. + err := f(conn) + if err != nil { + conn.Close() + } else { + select { + case p.ch <- conn: + default: + conn.Close() + } + } + + return err +} diff --git a/vendor/git.autistici.org/id/auth/lineproto/conn.go b/vendor/git.autistici.org/id/auth/lineproto/conn.go new file mode 100644 index 0000000000000000000000000000000000000000..25a6e6e6b25925bc8e2c3c238d909fcda787e5d8 --- /dev/null +++ b/vendor/git.autistici.org/id/auth/lineproto/conn.go @@ -0,0 +1,70 @@ +package lineproto + +import ( + "bufio" + "net" +) + +type Reader struct { + r *bufio.Reader +} + +func (r *Reader) ReadLine() ([]byte, error) { + var line []byte + for { + l, more, err := r.r.ReadLine() + if err != nil { + return nil, err + } + // Avoid the copy if the first call produced a full line. + if line == nil && !more { + return l, nil + } + line = append(line, l...) + if !more { + break + } + } + return line, nil +} + +type Writer struct { + w *bufio.Writer +} + +var crlf = []byte("\r\n") + +func (w *Writer) WriteLine(args ...[]byte) error { + for _, arg := range args { + _, err := w.w.Write(arg) + if err != nil { + return err + } + } + _, err := w.w.Write(crlf) + if err != nil { + return err + } + return w.w.Flush() +} + +type Conn struct { + net.Conn + *Reader + *Writer + + ServerName string +} + +func NewConn(c net.Conn, name string) *Conn { + return &Conn{ + Conn: c, + Reader: &Reader{r: bufio.NewReader(c)}, + Writer: &Writer{w: bufio.NewWriter(c)}, + ServerName: name, + } +} + +func (c *Conn) Close() error { + return c.Conn.Close() +} diff --git a/vendor/git.autistici.org/id/auth/lineproto/lineproto.go b/vendor/git.autistici.org/id/auth/lineproto/lineproto.go new file mode 100644 index 0000000000000000000000000000000000000000..967589f0f50b5cad636e861c989b46c99993de77 --- /dev/null +++ b/vendor/git.autistici.org/id/auth/lineproto/lineproto.go @@ -0,0 +1,133 @@ +package lineproto + +import ( + "context" + "errors" + "io" + "log" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// LineHandler is the handler for LineServer. +type LineHandler interface { + ServeLine(context.Context, LineResponseWriter, []byte) error +} + +// ErrCloseConnection must be returned by a LineHandler when we want +// to cleanly terminate the connection without raising an error. +var ErrCloseConnection = errors.New("close") + +// LineResponseWriter writes a single-line response to the underlying +// connection. +type LineResponseWriter interface { + // WriteLine writes a response as a single line (the line + // terminator is added by the function). + WriteLine(...[]byte) error +} + +// LineServer implements a line-based text protocol. It satisfies the +// Handler interface. +type LineServer struct { + handler LineHandler + + IdleTimeout time.Duration + WriteTimeout time.Duration + RequestTimeout time.Duration +} + +var ( + defaultIdleTimeout = 600 * time.Second + defaultWriteTimeout = 10 * time.Second + defaultRequestTimeout = 30 * time.Second +) + +// NewLineServer returns a new LineServer with the given handler and +// default I/O timeouts. +func NewLineServer(h LineHandler) *LineServer { + return &LineServer{ + handler: h, + IdleTimeout: defaultIdleTimeout, + WriteTimeout: defaultWriteTimeout, + RequestTimeout: defaultRequestTimeout, + } +} + +// ServeConnection handles a new connection. It will accept multiple +// requests on the same connection (or not, depending on the client +// preference). +func (l *LineServer) ServeConnection(c *Conn) { + totalConnections.WithLabelValues(c.ServerName).Inc() + for { + c.Conn.SetReadDeadline(time.Now().Add(l.IdleTimeout)) // nolint + line, err := c.ReadLine() + if err == io.EOF { + break + } else if err != nil { + if !strings.Contains(err.Error(), "use of closed network connection") { + log.Printf("client error: %v", err) + } + break + } + + // Create a context for the request and call the + // handler with it. Set a write deadline on the + // connection to allow the full RequestTimeout time to + // generate the response. + start := time.Now() + c.Conn.SetWriteDeadline(start.Add(l.RequestTimeout + l.WriteTimeout)) // nolint + ctx, cancel := context.WithTimeout(context.Background(), l.RequestTimeout) + err = l.handler.ServeLine(ctx, c, line) + elapsedMs := time.Since(start).Nanoseconds() / 1000000 + requestLatencyHist.WithLabelValues(c.ServerName). + Observe(float64(elapsedMs)) + cancel() + + // Close the connection on error, or on empty response. + if err != nil { + totalRequests.WithLabelValues(c.ServerName, "error").Inc() + if err != ErrCloseConnection { + log.Printf("request error: %v", err) + } + break + } + totalRequests.WithLabelValues(c.ServerName, "ok").Inc() + } +} + +// Instrumentation metrics. +var ( + totalConnections = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "lineproto_connections_total", + Help: "Total number of connections.", + }, + []string{"listener"}, + ) + totalRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "lineproto_requests_total", + Help: "Total number of requests.", + }, + []string{"listener", "status"}, + ) + // Histogram buckets are tuned for the low-milliseconds range + // (the largest bucket sits at ~1s). + requestLatencyHist = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "lineproto_requests_latency_ms", + Help: "Latency of requests.", + Buckets: prometheus.ExponentialBuckets(5, 1.4142, 16), + }, + []string{"listener"}, + ) +) + +func init() { + prometheus.MustRegister(totalConnections) + prometheus.MustRegister(totalRequests) + prometheus.MustRegister(requestLatencyHist) + +} diff --git a/vendor/git.autistici.org/id/auth/lineproto/server.go b/vendor/git.autistici.org/id/auth/lineproto/server.go new file mode 100644 index 0000000000000000000000000000000000000000..c2fb178ef175182cff4f1c25e86353761ad11c1a --- /dev/null +++ b/vendor/git.autistici.org/id/auth/lineproto/server.go @@ -0,0 +1,89 @@ +package lineproto + +import ( + "container/list" + "net" + "sync" + "sync/atomic" +) + +type Handler interface { + ServeConnection(c *Conn) +} + +type Server struct { + Name string + + l net.Listener + h Handler + + // Keep track of active connections so we can shut them down + // on Close. + closing atomic.Value + wg sync.WaitGroup + connMx sync.Mutex + conns list.List +} + +func NewServer(name string, l net.Listener, h Handler) *Server { + s := &Server{ + Name: name, + l: l, + h: h, + } + s.closing.Store(false) + return s +} + +// Close the socket listener and release all associated resources. +// Waits for active connections to terminate before returning. +func (s *Server) Close() { + s.closing.Store(true) + + // Close the listener to stop incoming connections. + s.l.Close() // nolint + + // Close all active connections (this will return an error to + // the client if the connection is not idle). + s.connMx.Lock() + for el := s.conns.Front(); el != nil; el = el.Next() { + el.Value.(net.Conn).Close() + } + s.connMx.Unlock() + + s.wg.Wait() +} + +func (s *Server) isClosing() bool { + return s.closing.Load().(bool) +} + +// Serve connections. +func (s *Server) Serve() error { + for { + conn, err := s.l.Accept() + if err != nil { + if s.isClosing() { + return nil + } + return err + } + + s.wg.Add(1) + + s.connMx.Lock() + connEl := s.conns.PushBack(conn) + s.connMx.Unlock() + + go func() { + s.h.ServeConnection(NewConn(conn, s.Name)) + conn.Close() // nolint + if !s.isClosing() { + s.connMx.Lock() + s.conns.Remove(connEl) + s.connMx.Unlock() + } + s.wg.Done() + }() + } +} diff --git a/vendor/git.autistici.org/id/auth/lineproto/unix.go b/vendor/git.autistici.org/id/auth/lineproto/unix.go new file mode 100644 index 0000000000000000000000000000000000000000..3a6378b492619900939412a51e2fd3b9e662523d --- /dev/null +++ b/vendor/git.autistici.org/id/auth/lineproto/unix.go @@ -0,0 +1,56 @@ +package lineproto + +import ( + "errors" + "net" + "os" + + "github.com/theckman/go-flock" +) + +type socketListener struct { + net.Listener + + lock *flock.Flock +} + +func (l *socketListener) Close() error { + defer l.lock.Unlock() // nolint: errcheck + return l.Listener.Close() +} + +// NewUNIXSocketListener creates a new net.Listener listening on a +// UNIX socket at the given path, with a filesystem-level lock. +func NewUNIXSocketListener(socketPath string) (net.Listener, error) { + // The simplest workflow is: create a new socket, remove it on + // exit. However, if the program crashes, the socket might + // stick around and prevent the next execution from starting + // successfully. We could remove it before starting, but that + // would be dangerous if another instance was listening on + // that socket. So we wrap socket access with a file lock. + lock := flock.New(socketPath + ".lock") + locked, err := lock.TryLock() + if err != nil { + return nil, err + } + if !locked { + return nil, errors.New("socket is locked by another process") + } + + addr, err := net.ResolveUnixAddr("unix", socketPath) + if err != nil { + lock.Unlock() + return nil, err + } + + // Always try to unlink the socket before creating it. + os.Remove(socketPath) // nolint + + l, err := net.ListenUnix("unix", addr) + if err != nil { + lock.Unlock() + return nil, err + } + + return &socketListener{Listener: l, lock: lock}, nil +} diff --git a/vendor/git.autistici.org/id/auth/server/authserver.go b/vendor/git.autistici.org/id/auth/server/authserver.go index d76eb553ed99ccb7e01635e5f6bd257d5d2710e8..22c734be13ea762e0ca49eac59e6744ac3385527 100644 --- a/vendor/git.autistici.org/id/auth/server/authserver.go +++ b/vendor/git.autistici.org/id/auth/server/authserver.go @@ -326,6 +326,11 @@ func (s *Server) Authenticate(ctx context.Context, req *auth.Request) *auth.Resp var ( errServiceUnknown = errors.New("unknown service") errUserUnknown = errors.New("unknown user") + errNoMechanisms = errors.New("no authentication mechanisms available") + errBadPassword = errors.New("wrong password") + errBadASP = errors.New("wrong app-specific password") + errBadU2F = errors.New("bad U2F response") + errBadOTP = errors.New("invalid OTP") ) // Function with the actual authentication API logic - we return both @@ -381,21 +386,37 @@ fail: // Authenticate a user. Returning an error should result in an // AuthResponse with StatusError. func (s *Server) authenticateUser(req *auth.Request, svc *service, user *backend.User) (resp *auth.Response, err error) { + err = errNoMechanisms + + // The service can override the ASP service name presented in + // the request. + aspService := req.Service + if svc.aspService != "" { + aspService = svc.aspService + } + // Verify different credentials depending on whether the user // has 2FA enabled or not, and on whether the service itself // supports challenge-response authentication. - if !svc.ignore2FA && (svc.enforce2FA || user.Has2FA()) { - if svc.challengeResponse { + if svc.ignore2FA { + resp, err = s.authenticateUserWithPassword(user, req) + } else if svc.challengeResponse { + if svc.enforce2FA || user.Has2FA() { resp, err = s.authenticateUserWith2FA(user, req) - } else { + } else if user.HasASPs(aspService) { + // It doesn't make much sense to support ASPs + // for a challenge-response service, but we + // still do it for completeness. + // // Rewrite the 'service' for app-specific // password matching, if necessary. - if svc.aspService != "" { - req.Service = svc.aspService - } - resp, err = s.authenticateUserWithASP(user, req) + resp, err = s.authenticateUserWithASP(user, req, aspService) + } else { + resp, err = s.authenticateUserWithPassword(user, req) } - } else { + } else if svc.enforce2FA || user.HasASPs(aspService) { + resp, err = s.authenticateUserWithASP(user, req, aspService) + } else if !user.Has2FA() { resp, err = s.authenticateUserWithPassword(user, req) } if err != nil { @@ -429,22 +450,22 @@ func (s *Server) authenticateUserWithPassword(user *backend.User, req *auth.Requ if checkPassword(req.Password, user.EncryptedPassword) { return newOK(), nil } - return nil, errors.New("wrong password") + return nil, errBadPassword } -func (s *Server) authenticateUserWithASP(user *backend.User, req *auth.Request) (*auth.Response, error) { +func (s *Server) authenticateUserWithASP(user *backend.User, req *auth.Request, aspService string) (*auth.Response, error) { for _, asp := range user.AppSpecificPasswords { - if asp.Service == req.Service && checkPassword(req.Password, asp.EncryptedPassword) { + if asp.Service == aspService && checkPassword(req.Password, asp.EncryptedPassword) { return newOK(), nil } } - return nil, errors.New("wrong app-specific password") + return nil, errBadASP } func (s *Server) authenticateUserWith2FA(user *backend.User, req *auth.Request) (*auth.Response, error) { // First of all verify the password. if !checkPassword(req.Password, user.EncryptedPassword) { - return nil, errors.New("wrong password") + return nil, errBadPassword } // If the request contains one of the 2FA attributes, verify @@ -456,7 +477,7 @@ func (s *Server) authenticateUserWith2FA(user *backend.User, req *auth.Request) if user.HasU2F() && s.checkU2F(user, req.U2FResponse) { return newOK(), nil } - return nil, errors.New("bad U2F response") + return nil, errBadU2F case req.OTP != "": if user.HasOTP() && s.checkOTP(user, req.OTP, user.TOTPSecret) { // Save the token for replay protection. @@ -465,7 +486,7 @@ func (s *Server) authenticateUserWith2FA(user *backend.User, req *auth.Request) } return newOK(), nil } - return nil, errors.New("bad OTP") + return nil, errBadOTP default: resp := &auth.Response{ Status: auth.StatusInsufficientCredentials, diff --git a/vendor/git.autistici.org/id/auth/server/unixserver.go b/vendor/git.autistici.org/id/auth/server/unixserver.go index fe08d6a0e36a9b1ec3a3f39f9adb4b7636f8cf3a..f27a237236da925eaf2ea35d1b105e0926e17d9d 100644 --- a/vendor/git.autistici.org/id/auth/server/unixserver.go +++ b/vendor/git.autistici.org/id/auth/server/unixserver.go @@ -6,8 +6,8 @@ import ( "errors" "fmt" - "git.autistici.org/ai3/go-common/unix" "git.autistici.org/id/auth" + "git.autistici.org/id/auth/lineproto" ) // SocketServer accepts connections on a UNIX socket, speaking the @@ -28,7 +28,7 @@ func NewSocketServer(authServer *Server) *SocketServer { // ServeLine handles a single request and writes a // response. Implements the unix.LineHandler interface. -func (s *SocketServer) ServeLine(ctx context.Context, lw unix.LineResponseWriter, line []byte) error { +func (s *SocketServer) ServeLine(ctx context.Context, lw lineproto.LineResponseWriter, line []byte) error { // Parse the incoming command. The only two known // commands are 'auth' for an authentication request, // and 'quit' to terminate the connection (closing the @@ -42,7 +42,7 @@ func (s *SocketServer) ServeLine(ctx context.Context, lw unix.LineResponseWriter cmd := string(parts[0]) switch { case nargs == 1 && cmd == "quit": - return unix.ErrCloseConnection + return lineproto.ErrCloseConnection case nargs == 2 && cmd == "auth": return s.handleAuth(ctx, lw, parts[1]) default: @@ -50,7 +50,7 @@ func (s *SocketServer) ServeLine(ctx context.Context, lw unix.LineResponseWriter } } -func (s *SocketServer) handleAuth(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error { +func (s *SocketServer) handleAuth(ctx context.Context, lw lineproto.LineResponseWriter, arg []byte) error { var req auth.Request if err := s.codec.Decode(arg, &req); err != nil { return fmt.Errorf("decoding error: %v", err) @@ -58,5 +58,5 @@ func (s *SocketServer) handleAuth(ctx context.Context, lw unix.LineResponseWrite resp := s.auth.Authenticate(ctx, &req) - return lw.WriteLineCRLF(s.codec.Encode(resp)) + return lw.WriteLine(s.codec.Encode(resp)) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 5038e938f058623be93a01c626ecf26f2e4613b1..6b4df19484fd21d712e2e1628ef0462a8102ca7a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,9 +12,10 @@ git.autistici.org/ai3/go-common/tracing git.autistici.org/ai3/go-common/unix # git.autistici.org/ai3/tools/webappdb v0.0.0-20200630154205-9f45389dbd94 git.autistici.org/ai3/tools/webappdb/proto -# git.autistici.org/id/auth v0.0.0-20200212081728-3d44524ae2e5 +# git.autistici.org/id/auth v0.0.0-20210109163523-998aa7f16d74 git.autistici.org/id/auth git.autistici.org/id/auth/client +git.autistici.org/id/auth/lineproto git.autistici.org/id/auth/server git.autistici.org/id/auth/backend git.autistici.org/id/auth/backend/file