git » chasquid » commit cfe0e48

auth: Allow users without a domain

author Alberto Bertogli
2021-06-11 19:05:41 UTC
committer Alberto Bertogli
2021-06-11 19:09:15 UTC
parent 099e2e2269d8bc7b315dce596b4b92fdf46555f2

auth: Allow users without a domain

Some deployments already have users that authenticate without a domain.
Today, we refuse to even consider those, and reject them at parsing time.

However, it is a use-case worth supporting, at least with some
restrictions that make the complexity manageable.

This patch changes the auth package to support authenticating users
without an "@domain" part.

Those requests will always be directly passed on to the fallback
authenticator, if available.

The dovecot fallback authenticator can already handle this case just fine.

docs/dovecot.md +3 -2
internal/auth/auth.go +21 -14
internal/auth/auth_test.go +2 -1
test/t-11-dovecot/config/dovecot.conf.in +3 -2
test/t-11-dovecot/config/passwd +2 -1
test/t-11-dovecot/msmtprc +5 -0
test/t-11-dovecot/run.sh +12 -1

diff --git a/docs/dovecot.md b/docs/dovecot.md
index a40cd77..853e7cc 100644
--- a/docs/dovecot.md
+++ b/docs/dovecot.md
@@ -31,8 +31,9 @@ accordingly.
 
 This lets chasquid issue authentication requests to dovecot.
 
-Authentication requests sent by chasquid to dovecot will use the
-fully-qualified user form, `user@domain`.
+Authentication requests sent by chasquid to dovecot will pass on the username
+as specified by the client. This will usually be either `user@domain`, or just
+`user`.
 
 
 ## Configuring chasquid
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index 1a76e8c..4f02fdf 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -42,7 +42,7 @@ type Authenticator struct {
 	// Fallback backend, to use when backends[domain] (which may not exist)
 	// did not yield a positive result.
 	// Note that this backend gets the user with the domain included, of the
-	// form "user@domain".
+	// form "user@domain" (if available).
 	Fallback Backend
 
 	// How long Authenticate calls should last, approximately.
@@ -90,7 +90,11 @@ func (a *Authenticator) Authenticate(user, domain, password string) (bool, error
 	}
 
 	if a.Fallback != nil {
-		ok, err := a.Fallback.Authenticate(user+"@"+domain, password)
+		id := user
+		if domain != "" {
+			id = user + "@" + domain
+		}
+		ok, err := a.Fallback.Authenticate(id, password)
 		tr.Debugf("Fallback: %v %v", ok, err)
 		return ok, err
 	}
@@ -113,7 +117,11 @@ func (a *Authenticator) Exists(user, domain string) (bool, error) {
 	}
 
 	if a.Fallback != nil {
-		ok, err := a.Fallback.Exists(user + "@" + domain)
+		id := user
+		if domain != "" {
+			id = user + "@" + domain
+		}
+		ok, err := a.Fallback.Exists(id)
 		tr.Debugf("Fallback: %v %v", ok, err)
 		return ok, err
 	}
@@ -158,9 +166,11 @@ func (a *Authenticator) Reload() error {
 //
 // https://tools.ietf.org/html/rfc4954#section-4.1.
 //
-// Either both ID match, or one of them is empty.
-// We expect the ID to be "user@domain", which is NOT an RFC requirement but
-// our own.
+// Either both IDs match, or one of them is empty.
+//
+// We split the id into user@domain, since in most cases we expect that to be
+// the used form, and normalize them. If there is no domain, we just return
+// "" for it. The rest of the stack will know how to handle it.
 func DecodeResponse(response string) (user, domain, passwd string, err error) {
 	buf, err := base64.StdEncoding.DecodeString(response)
 	if err != nil {
@@ -201,17 +211,14 @@ func DecodeResponse(response string) (user, domain, passwd string, err error) {
 		return
 	}
 
-	// Identity must be in the form "user@domain".
-	// This is NOT an RFC requirement, it's our own.
+	// Split identity into "user@domain", if possible.
+	user = identity
 	idsp := strings.SplitN(identity, "@", 2)
-	if len(idsp) != 2 {
-		err = fmt.Errorf("identity must be in the form user@domain")
-		return
+	if len(idsp) >= 2 {
+		user = idsp[0]
+		domain = idsp[1]
 	}
 
-	user = idsp[0]
-	domain = idsp[1]
-
 	// Normalize the user and domain. This is so users can write the username
 	// in their own style and still can log in.  For the domain, we use IDNA
 	// and relevant transformations to turn it to utf8 which is what we use
diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go
index 0a1a124..ce1b840 100644
--- a/internal/auth/auth_test.go
+++ b/internal/auth/auth_test.go
@@ -19,6 +19,7 @@ func TestDecodeResponse(t *testing.T) {
 		{"dUBkAABwYXNz", "u", "d", "pass"},         // u@d\0\0pass
 		{"AHVAZABwYXNz", "u", "d", "pass"},         // \0u@d\0pass
 		{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
+		{"dQB1AHBhc3M=", "u", "", "pass"},          // u\0u\0pass
 
 		// "ñaca@ñeque\0\0clavaré"
 		{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
@@ -42,7 +43,7 @@ func TestDecodeResponse(t *testing.T) {
 
 	failedCases := []string{
 		"", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00",
-		"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass", "a\x00a\x00pass",
+		"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass",
 		"\xffa@b\x00\xffa@b\x00pass",
 	}
 	for _, c := range failedCases {
diff --git a/test/t-11-dovecot/config/dovecot.conf.in b/test/t-11-dovecot/config/dovecot.conf.in
index 77fbca3..9f9d962 100644
--- a/test/t-11-dovecot/config/dovecot.conf.in
+++ b/test/t-11-dovecot/config/dovecot.conf.in
@@ -5,10 +5,11 @@ ssl = no
 default_internal_user = $USER
 default_login_user = $USER
 
-# Before auth checks, rename "u@d" to "u-AT-d". This exercises that chasquid
+# Before auth checks, rename "u@d" to "u-x". This exercises that chasquid
 # handles well the case where the returned user information does not match the
 # requested user.
-auth_username_format = "%n-AT-%d"
+# We drop the domain, to exercise "naked" auth handling.
+auth_username_format = "%n-x"
 
 passdb {
 	driver = passwd-file
diff --git a/test/t-11-dovecot/config/passwd b/test/t-11-dovecot/config/passwd
index 2ceb0ee..61777ba 100644
--- a/test/t-11-dovecot/config/passwd
+++ b/test/t-11-dovecot/config/passwd
@@ -1 +1,2 @@
-user-AT-srv:{plain}password:1000:1000::/home/user
+user-x:{plain}password:1000:1000::/home/user
+naked-x:{plain}gun:1001:1001::/home/naked
diff --git a/test/t-11-dovecot/msmtprc b/test/t-11-dovecot/msmtprc
index 99c540d..54b7338 100644
--- a/test/t-11-dovecot/msmtprc
+++ b/test/t-11-dovecot/msmtprc
@@ -26,3 +26,8 @@ password secretpassword
 account badpasswd : default
 user user@srv
 password badsecretpassword
+
+account naked : default
+from naked@srv
+user naked
+password gun
diff --git a/test/t-11-dovecot/run.sh b/test/t-11-dovecot/run.sh
index e93e0f2..0be1339 100755
--- a/test/t-11-dovecot/run.sh
+++ b/test/t-11-dovecot/run.sh
@@ -51,11 +51,22 @@ mkdir -p .logs
 chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
 wait_until_ready 1025
 
-# Send an email as user@srv successfully.
+# Send an email as "user@srv" successfully.
 run_msmtp user@srv < content
 wait_for_file .mail/user@srv
 mail_diff content .mail/user@srv
 
+# Send an email as "naked" successfully.
+rm .mail/user@srv
+run_msmtp -a naked user@srv < content
+wait_for_file .mail/user@srv
+mail_diff content .mail/user@srv
+
+# Send an email to the "naked" user successfully.
+run_msmtp naked@srv < content
+wait_for_file .mail/naked@srv
+mail_diff content .mail/naked@srv
+
 # Fail to send to nobody@srv (user does not exist).
 if run_msmtp nobody@srv < content 2> /dev/null; then
 	fail "successfuly sent an email to a non-existent user"