From d0fa32ce2a25ee6bf60d289bba40d78d9e21f202 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 4 May 2019 09:02:33 +0100
Subject: [PATCH] Update to new id/auth API that returns all supported 2FA
 methods

First step towards letting users pick the method they prefer.
---
 server/http_test.go                          |   4 +-
 server/login.go                              |  29 ++--
 vendor/git.autistici.org/id/auth/README.md   | 166 ++++++++++++++++---
 vendor/git.autistici.org/id/auth/protocol.go |  93 ++++++++---
 vendor/vendor.json                           |  10 +-
 5 files changed, 233 insertions(+), 69 deletions(-)

diff --git a/server/http_test.go b/server/http_test.go
index c4020ac..fa711be 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -32,8 +32,8 @@ func (c *fakeAuthClient) Authenticate(_ context.Context, req *auth.Request) (*au
 		return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil
 	case req.Username == "test2fa" && p == "password":
 		return &auth.Response{
-			Status:    auth.StatusInsufficientCredentials,
-			TFAMethod: auth.TFAMethodOTP,
+			Status:     auth.StatusInsufficientCredentials,
+			TFAMethods: []auth.TFAMethod{auth.TFAMethodOTP},
 		}, nil
 	}
 
diff --git a/server/login.go b/server/login.go
index cc7f5f0..e511e8e 100644
--- a/server/login.go
+++ b/server/login.go
@@ -35,12 +35,8 @@ type loginSession struct {
 	Username string
 	Password string
 
-	// Cached from the first auth.Response.
-	U2FSignRequest *u2f.WebSignRequest
-
-	// Never actually serialized, just passed on
-	// loginStateSuccess.
-	UserInfo *auth.UserInfo
+	// The auth.Response is cached for 2FA.
+	AuthResponse *auth.Response
 }
 
 var defaultLoginSessionLifetime = 300 * time.Second
@@ -165,7 +161,7 @@ func (l *loginHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
-			if err := l.loginCallback(w, req, session.Username, session.Password, session.UserInfo); err != nil {
+			if err := l.loginCallback(w, req, session.Username, session.Password, session.AuthResponse.UserInfo); err != nil {
 				log.Printf("login callback error: %v: user=%s", err, session.Username)
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
@@ -222,18 +218,19 @@ func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request,
 		case auth.StatusOK:
 			session.Username = username
 			session.Password = password
-			session.UserInfo = resp.UserInfo
+			session.AuthResponse = resp
 			return loginStateSuccess, nil, nil
 		case auth.StatusInsufficientCredentials:
 			session.Username = username
 			session.Password = password
-			// If there is a U2F challenge in the auth
-			// response, store it so we can render it
-			// later.
+			session.AuthResponse = resp
+
+			// Always prefer U2F if supported, default to OTP.  We
+			// are assuming that the auth.Response is well formed,
+			// and TFAMethods is not nil.
 			var nextState loginState = loginStateOTP
-			if resp.TFAMethod == auth.TFAMethodU2F {
+			if resp.Has2FAMethod(auth.TFAMethodU2F) {
 				nextState = loginStateU2F
-				session.U2FSignRequest = resp.U2FSignRequest
 			}
 			return nextState, nil, nil
 		}
@@ -255,7 +252,7 @@ func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, sessi
 			return loginStateNone, nil, err
 		}
 		if resp.Status == auth.StatusOK {
-			session.UserInfo = resp.UserInfo
+			session.AuthResponse = resp
 			return loginStateSuccess, nil, nil
 		}
 		env["Error"] = true
@@ -270,7 +267,7 @@ func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, sessi
 	u2fresponse := req.FormValue("u2f_response")
 
 	env := map[string]interface{}{
-		"U2FSignRequest": session.U2FSignRequest,
+		"U2FSignRequest": session.AuthResponse.U2FSignRequest,
 		"Error":          false,
 	}
 	if req.Method == "POST" && u2fresponse != "" {
@@ -284,7 +281,7 @@ func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, sessi
 			return loginStateNone, nil, err
 		}
 		if resp.Status == auth.StatusOK {
-			session.UserInfo = resp.UserInfo
+			session.AuthResponse = resp
 			return loginStateSuccess, nil, nil
 		}
 		env["Error"] = true
diff --git a/vendor/git.autistici.org/id/auth/README.md b/vendor/git.autistici.org/id/auth/README.md
index cf5eead..b25a67f 100644
--- a/vendor/git.autistici.org/id/auth/README.md
+++ b/vendor/git.autistici.org/id/auth/README.md
@@ -64,12 +64,14 @@ The YAML file should contain a dictionary with the following attributes:
   file (using the `services` attribute), it is possible to define
   additional services in YAML-encoded files (having a *.yml*
   extension), which is more automation-friendly.
+* `backends` is a dictionary describing all known backends and their
+  configuration parameters. The *file* backend is predefined and always
+  exists (it requires no configuration).
+* `backends_dir` (optional) points at a directory containing backend
+  configuration as YAML-encoded files: all files with a *.yml*
+  extension will be loaded.
 * `rate_limits` defines the global rate limiters and blacklists. See
   the *Rate limiting* section below.
-* `enabled_backends` is the list of user backends that should be
-  enabled (the available backends are *file* and *ldap*)
-* `ldap_config` specifies the configuration for the LDAP backend --
-  see the *LDAP Backend* section below
 * `user_meta_server` holds the configuration for the user-meta-server
   backend:
   * `url` is the URL of the service
@@ -80,6 +82,28 @@ The YAML file should contain a dictionary with the following attributes:
 * `memcache_servers` contains a list of memcached server addresses (in
   host:port format)
 
+## Example configuration
+
+An example configuration using both the *sql* backend (with default
+schema and queries) for normal users, and the *file* backend for admin
+users could look like this:
+
+```yaml
+---
+backends:
+  sql:
+    driver: sqlite3
+    db_uri: users.db
+services:
+  example_service:
+    backends:
+      - backend: sql
+      - backend: file
+        params:
+          src: admins.yml
+        static_groups: [admins]
+```
+
 ## Rate limiting
 
 Rate limits and blacklists are global (available to all services), to
@@ -103,12 +127,12 @@ should specify the following attributes:
 
 Each service definition is a dictionary with the following attributes:
 
-* `backends` is a list of user backend specifications, each one should
-  include a backend-specific configuration under an attribute named
-  after the backend itself:
-  * `file` is simply a path to a user list file, see the *File
-    backend* section below
-  * `ldap` configues the LDAP backend for this service
+* `backends` is a list of user backend specifications, each one a
+  dictionary/map with the following attributes:
+  * `backend` must be the name of a backend that appears in the
+    top-level configuration map *backends*.
+  * `params` is a map of backend-specific attributes that configure
+    the backend for this service.
   * `static_groups` is a list of group names that users sourced from
     this backend will automatically be added to
 * `challenge_response` is a boolean parameter that, when true, enables
@@ -116,6 +140,8 @@ Each service definition is a dictionary with the following attributes:
   only for interactive services)
 * `enforce_2fa` is a boolean flag that, when true, will disable
   non-2FA logins for this service
+* `enable_last_login_reporting` is a boolean flag that enables last login
+  reporting to usermetadb
 * `enable_device_tracking` is a boolean flag that enables device
   tracking for this service (assuming the client provides device
   information)
@@ -125,30 +151,34 @@ Each service definition is a dictionary with the following attributes:
 ## File backend
 
 The *file* backend reads users and their credentials from a
-YAML-encoded file. This file should contain a list of dictionaries,
-each representing a user, with the following attributes:
+YAML-encoded file. The service-specific configuration parameters are:
+
+* `src` should point at the users file.
+
+This file should contain a list of dictionaries, each representing a
+user, with the following attributes:
 
 * `name` is the username
 * `email` is the email associated with the user (optional)
 * `password` stores the encrypted password
 * `totp_secret` stores the *unencrypted* TOTP secret seed
+* `u2f_registrations` is a list of U2F registrations with `key_handle`
+  and `public_key` attributes, in the format used by *pamu2fcfg* (for
+  convenience)
 * `groups` is a list of group names that the user belongs to
 
-The file backend only supports TOTP as a two-factor authentication
-method, U2F support is currently missing.
-
-## LDAP Backend
+## LDAP backend
 
 The *ldap* backend will look up user information in a LDAP database.
-It needs some parameters to be passed in the top-level *ldap_config*
-dictionary:
+The backend connects to a single LDAP server and requires the
+following top-level configuration:
 
 * `uri` of the LDAP server (like *ldapi:///var/run/ldap/ldapi*)
 * `bind_dn` is the DN to bind with
 * `bind_pw_file` points at a file containing the bind password
 
-The *ldap* backend will currently always attempt to bind on every
-connection.
+Each service can then use different queries, as shown in the next
+section.
 
 ### Query definition
 
@@ -201,6 +231,102 @@ App-specific passwords should be encoded as colon-separated strings:
 The password should be encrypted. The comment is a free-form string
 set by the user to tell the various credentials apart.
 
+## SQL backend
+
+The SQL backend allows you to use a SQL database to store user
+information. It can adapt to any schema, provided that you can write
+the queries it expects.
+
+The parameters for the SQL backend configuration are:
+
+* `driver` is the name of the database/sql driver (currently it must
+  be one of `sqlite3`, `mysql` or `postgres`, the built-in drivers)
+* `db_uri` is the database URI (a.k.a. DSN), whose exact syntax will
+  depend on the chosen driver. Check out the documentation for the
+  database/sql [sqlite](https://github.com/mattn/go-sqlite3),
+  [mysql](https://github.com/go-sql-driver/mysql) and
+  [postgres](https://godoc.org/github.com/lib/pq) drivers.
+
+### Query definition
+
+Each service can specify a set of different SQL queries. It can be
+configured with the following attributes:
+
+* `queries` holds the map of SQL queries that tell the auth-server
+  how to query your database.
+
+The known queries are identified by name. It does not matter what
+operations you do as long as the queries take the expected input
+substitution parameters, and return rows with the expected number of
+fields (column names do not matter). You should use the parameter
+substitution symbol `?` as placeholder for query parameters.
+
+* `get_user` takes a single parameter (the user name) and must return
+  a single row with *email*, *password*, *TOTP secret* and *shard*
+  fields for the matching user.
+* `get_user_groups` takes a single parameter (the user name) and must
+  return rows with a single *group_name* field corresponding to the
+  user's group memberships.
+* `get_user_u2f` takes a single parameter (user name) and must return
+  the user's U2F registrations as rows with *public_key* and
+  *key_handle* fields, in their native binary format.
+* `get_user_asp` takes a single parameter (user name) and must return
+  the user's application-specific passwords as rows with *service* and
+  *password* fields.
+
+The only mandatory query is *get_user*, if the other ones are not
+specified the associated fields will be empty.
+
+### Example database schema
+
+The following could be a (very simple) example database schema for a
+case where usernames are also email addresses, with support for all
+authentication features:
+
+```sql
+CREATE TABLE users (
+    email text NOT NULL,
+    password text NOT NULL,
+    totp_secret text,
+    shard text
+);
+CREATE UNIQUE INDEX users_email_idx ON users(email);
+CREATE TABLE group_memberships (
+    email text NOT NULL,
+    group_name text NOT NULL
+);
+CREATE INDEX group_memberships_idx ON group_memberships(email);
+CREATE TABLE u2f_registrations (
+    email text NOT NULL,
+    key_handle blob NOT NULL,
+    public_key blob NOT NULL
+);
+CREATE INDEX u2f_registrations_idx ON u2f_registrations(email);
+CREATE TABLE service_passwords (
+    email text NOT NULL,
+    service text NOT NULL,
+    password text NOT NULL
+);
+CREATE INDEX service_passwords_idx ON service_passwords(email);
+```
+
+With this schema, one could use the following configuration for a
+service:
+
+```yaml
+services:
+  example:
+    challenge_response: true
+    backends:
+      - backend: sql
+        params:
+          queries:
+            get_user: "SELECT email, password, totp_secret, shard FROM users WHERE email = ?"
+            get_user_groups: "SELECT group_name FROM group_memberships WHERE email = ?"
+            get_user_u2f: "SELECT public_key, key_handle FROM u2f_registrations WHERE email = ?"
+            get_user_asp: "SELECT service, password FROM service_passwords WHERE email = ?"
+```
+
 
 # Usage
 
diff --git a/vendor/git.autistici.org/id/auth/protocol.go b/vendor/git.autistici.org/id/auth/protocol.go
index 3db526f..2c0896a 100644
--- a/vendor/git.autistici.org/id/auth/protocol.go
+++ b/vendor/git.autistici.org/id/auth/protocol.go
@@ -10,13 +10,13 @@ import (
 // simple persistent cookie to track the same client device across
 // multiple session.
 type DeviceInfo struct {
-	ID         string
-	RemoteAddr string
-	RemoteZone string
-	UserAgent  string
-	Browser    string
-	OS         string
-	Mobile     bool
+	ID         string `json:"id"`
+	RemoteAddr string `json:"remote_addr"`
+	RemoteZone string `json:"remote_zone"`
+	UserAgent  string `json:"user_agent"`
+	Browser    string `json:"browser"`
+	OS         string `json:"os"`
+	Mobile     bool   `json:"mobile"`
 }
 
 func (d *DeviceInfo) encodeToMap(m map[string]string, prefix string) {
@@ -58,7 +58,6 @@ type Request struct {
 	U2FAppID    string
 	U2FResponse *u2f.SignResponse
 	DeviceInfo  *DeviceInfo
-	//Extra       map[string]string
 }
 
 func (r *Request) EncodeToMap(m map[string]string, prefix string) {
@@ -98,6 +97,25 @@ type UserInfo struct {
 	Groups []string
 }
 
+func encodeStringList(m map[string]string, prefix string, l []string) {
+	for i, elem := range l {
+		m[fmt.Sprintf("%s.%d.", prefix, i)] = elem
+	}
+}
+
+func decodeStringList(m map[string]string, prefix string) (out []string) {
+	i := 0
+	for {
+		s, ok := m[fmt.Sprintf("%s.%d.", prefix, i)]
+		if !ok {
+			break
+		}
+		out = append(out, s)
+		i++
+	}
+	return
+}
+
 func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
 	if u.Email != "" {
 		m[prefix+"email"] = u.Email
@@ -105,24 +123,14 @@ func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) {
 	if u.Shard != "" {
 		m[prefix+"shard"] = u.Shard
 	}
-	for i, g := range u.Groups {
-		m[fmt.Sprintf("%sgroup.%d.", prefix, i)] = g
-	}
+	encodeStringList(m, prefix+"group", u.Groups)
 }
 
 func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
 	u := UserInfo{
-		Email: m[prefix+"email"],
-		Shard: m[prefix+"shard"],
-	}
-	i := 0
-	for {
-		s, ok := m[fmt.Sprintf("%sgroup.%d.", prefix, i)]
-		if !ok {
-			break
-		}
-		u.Groups = append(u.Groups, s)
-		i++
+		Email:  m[prefix+"email"],
+		Shard:  m[prefix+"shard"],
+		Groups: decodeStringList(m, prefix+"group"),
 	}
 	if u.Email == "" && u.Shard == "" && len(u.Groups) == 0 {
 		return nil
@@ -133,16 +141,49 @@ func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo {
 // Response to an authentication request.
 type Response struct {
 	Status         Status
-	TFAMethod      TFAMethod
+	TFAMethods     []TFAMethod
 	U2FSignRequest *u2f.WebSignRequest
 	UserInfo       *UserInfo
 }
 
+// Has2FAMethod checks for the presence of a two-factor authentication
+// method in the Response.
+func (r *Response) Has2FAMethod(needle TFAMethod) bool {
+	for _, m := range r.TFAMethods {
+		if m == needle {
+			return true
+		}
+	}
+	return false
+}
+
+func encodeTFAMethodList(m map[string]string, prefix string, l []TFAMethod) {
+	if len(l) == 0 {
+		return
+	}
+	tmp := make([]string, 0, len(l))
+	for _, el := range l {
+		tmp = append(tmp, string(el))
+	}
+	encodeStringList(m, prefix, tmp)
+}
+
+func decodeTFAMethodList(m map[string]string, prefix string) []TFAMethod {
+	l := decodeStringList(m, prefix)
+	if len(l) == 0 {
+		return nil
+	}
+	out := make([]TFAMethod, 0, len(l))
+	for _, el := range l {
+		out = append(out, TFAMethod(el))
+	}
+	return out
+}
+
 func (r *Response) EncodeToMap(m map[string]string, prefix string) {
 	m[prefix+"status"] = r.Status.String()
-	m[prefix+"2fa_method"] = string(r.TFAMethod)
+	encodeTFAMethodList(m, prefix+"2fa_methods", r.TFAMethods)
 	if r.U2FSignRequest != nil {
-		// External type.
 		encodeU2FSignRequestToMap(r.U2FSignRequest, m, prefix+"u2f_req.")
 	}
 	if r.UserInfo != nil {
@@ -152,7 +193,7 @@ func (r *Response) EncodeToMap(m map[string]string, prefix string) {
 
 func (r *Response) DecodeFromMap(m map[string]string, prefix string) {
 	r.Status = parseAuthStatus(m[prefix+"status"])
-	r.TFAMethod = TFAMethod(m[prefix+"2fa_method"])
+	r.TFAMethods = decodeTFAMethodList(m, prefix+"2fa_methods")
 	r.U2FSignRequest = decodeU2FSignRequestFromMap(m, prefix+"u2f_req.")
 	r.UserInfo = decodeUserInfoFromMap(m, prefix+"user.")
 }
diff --git a/vendor/vendor.json b/vendor/vendor.json
index cfdc0f1..0d52d1e 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -27,16 +27,16 @@
 			"revisionTime": "2019-01-29T12:17:45Z"
 		},
 		{
-			"checksumSHA1": "6D5Xt9WoGSeTJE3XFw6P2/nKYrQ=",
+			"checksumSHA1": "T9WPwUls+LPk89st6TGCbQf5HNQ=",
 			"path": "git.autistici.org/id/auth",
-			"revision": "1276ec2bd95945ea45a8680a15b76cad8a339096",
-			"revisionTime": "2018-11-22T22:30:45Z"
+			"revision": "b9fd25734d1e2a7f92f32ab982f9c55fd9f9ef24",
+			"revisionTime": "2019-05-04T07:53:15Z"
 		},
 		{
 			"checksumSHA1": "t3JTZ0bAMQit79HYbcEykC8uxro=",
 			"path": "git.autistici.org/id/auth/client",
-			"revision": "1276ec2bd95945ea45a8680a15b76cad8a339096",
-			"revisionTime": "2018-11-22T22:30:45Z"
+			"revision": "b9fd25734d1e2a7f92f32ab982f9c55fd9f9ef24",
+			"revisionTime": "2019-05-04T07:53:15Z"
 		},
 		{
 			"checksumSHA1": "MlpsZgRytv/c9IX9YawRJDN/ibQ=",
-- 
GitLab