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