CVE-2023-27582: Improper Authentication (CWE-287)
value: maddy is a composable, all-in-one mail server. Starting with version 0.2.0 and prior to version 0.6.3, maddy allows a full authentication bypass if SASL authorization username is specified when using the PLAIN authentication mechanisms. Instead of validating the specified username, it is accepted as is after checking the credentials for the authentication username. maddy 0.6.3 includes the fix for the bug. There are no known workarounds.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 9.8
Published: 2023-03-13T22:15Z
Repository: https://github.com/foxcpp/maddy
Language: Go
Files changed: docs/man/maddy-auth.5.scd, internal/auth/external/externalauth.go, internal/auth/pam/module.go, internal/auth/plain_separate/plain_separate.go, internal/auth/plain_separate/plain_separate_test.go, internal/auth/sasl.go, internal/auth/sasl_test.go, internal/auth/shadow/module.go, internal/config/module/interfaces.go, internal/endpoint/imap/imap.go, internal/endpoint/smtp/smtp.go, internal/module/auth.go, internal/module/dummy.go, internal/storage/sql/sql.go
Commit date: 2020-02-28 01:38:40 +0300
Commit message: Revert authorization/authentication split

Authentication provider module is responsible only for authentication.
Nothing more. Access control (authorization) should be kept separate.

Diff: diff --git a/docs/man/maddy-auth.5.scd b/docs/man/maddy-auth.5.scd
index df52ac5..4f47efe 100644
--- a/docs/man/maddy-auth.5.scd
+++ b/docs/man/maddy-auth.5.scd
@@ -14,12 +14,11 @@ Most likely, you are going to use these modules with 'auth' directive of IMAP
 # SQL module (sql)
 
 sql module described in *maddy-storage*(5) can also be used as a authentication
 backend.
 
-The authorization identity is required to be a valid RFC 5321 e-mail address.
-It is returned as the authorization identity.
+The username is required to be a valid RFC 5321 e-mail address.
 
 # External authentication module (extauth)
 
 Module for authentication using external helper binary. It looks for binary
 named maddy-auth-helper in $PATH and libexecdir and uses it for authentication
@@ -30,12 +29,10 @@ Program is launched for each authentication. Username and password are written
 to stdin, adding \\n to the end. If binary exits with 0 status code -
 authentication is considered successful. If the status code is 1 -
 authentication is failed. If the status code is 2 - another unrelated error has
 happened. Additional information should be written to stderr.
 
-The authorization identtity is the same as authorization identity.
-
 ```
 extauth {
     helper /usr/bin/ldap-helper
     perdomain no
     domains example.org
@@ -77,17 +74,14 @@ maddy should be built with libpam build tag to use this module without
 'use_helper' directive.
 ```
 go get -tags 'libpam' ...
 ```
 
-The authorization identtity is the same as authorization identity.
-
 ```
 pam {
     debug no
     use_helper no
-	expect_address yes
 }
 ```
 
 ## Configuration directives
 
@@ -107,23 +101,21 @@ You need to use that if:
 privileges (e.g. when using system accounts).
 
 For 2, you need to make maddy-pam-helper binary setuid, see
 README.md in source tree for details.
 
-TL;DR (assuming you have maddy group):
+TL;DR (assuming you have the maddy group):
 ```
 chown root:maddy /usr/lib/maddy/maddy-pam-helper
 chmod u+xs,g+x,o-x /usr/lib/maddy/maddy-pam-helper
 ```
 
 # Shadow database authentication module (shadow)
 
 Implements authentication by reading /etc/shadow. Alternatively it can be
 configured to use helper binary like extauth does.
 
-The authorization identtity is the same as authorization identity.
-
 ```
 shadow {
     debug no
     use_helper no
 }
@@ -173,18 +165,11 @@ plain_separate {
 How it works:
 1. Initial username input is normalized using PRECIS UsernameCaseMapped profile.
 2. Each table specified with the 'user' directive looked up using normalized
    username. If match is not found in any table, authentication fails.
 3. Each authentication provider specified with the 'pass' directive is tried.
-   First match wins same as in step 2.
-
-The set of authorization identities estabilished by plain_separate is the list of
-strings found in 'user' tables. If there are no 'user' tables (only 'pass'
-directives are specified), the list of authorization identities returned by
-auth. providers is used.
-*Note*: If a username table returns empty string, the authorization identity is
-assumed to be the same as authentication identity.
+   If authentication with all providers fails - an error is returned.
 
 ## Configuration directives
 
 **Syntax:** user _table module_
 
diff --git a/internal/auth/external/externalauth.go b/internal/auth/external/externalauth.go
index 4a34e33..e7002a0 100644
--- a/internal/auth/external/externalauth.go
+++ b/internal/auth/external/externalauth.go
@@ -69,18 +69,17 @@ func (ea *ExternalAuth) Init(cfg *config.Map) error {
 	ea.Log.Debugln("using helper:", ea.helperPath)
 
 	return nil
 }
 
-func (ea *ExternalAuth) AuthPlain(username, password string) ([]string, error) {
+func (ea *ExternalAuth) AuthPlain(username, password string) error {
 	accountName, ok := auth.CheckDomainAuth(username, ea.perDomain, ea.domains)
 	if !ok {
-		return nil, module.ErrUnknownCredentials
+		return module.ErrUnknownCredentials
 	}
 
-	// TODO: Extend process protocol to support multiple authorization identities.
-	return []string{username}, AuthUsingHelper(ea.helperPath, accountName, password)
+	return AuthUsingHelper(ea.helperPath, accountName, password)
 }
 
 func init() {
 	module.Register("extauth", NewExternalAuth)
 }
diff --git a/internal/auth/pam/module.go b/internal/auth/pam/module.go
index ca42340..c31b821 100644
--- a/internal/auth/pam/module.go
+++ b/internal/auth/pam/module.go
@@ -56,21 +56,21 @@ func (a *Auth) Init(cfg *config.Map) error {
 	}
 
 	return nil
 }
 
-func (a *Auth) AuthPlain(username, password string) ([]string, error) {
+func (a *Auth) AuthPlain(username, password string) error {
 	if a.useHelper {
 		if err := external.AuthUsingHelper(a.helperPath, username, password); err != nil {
-			return nil, err
+			return err
 		}
 	}
 	err := runPAMAuth(username, password)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	return []string{username}, nil
+	return nil
 }
 
 func init() {
 	module.Register("pam", New)
 }
diff --git a/internal/auth/plain_separate/plain_separate.go b/internal/auth/plain_separate/plain_separate.go
index 2e6e0c5..3f310f6 100644
--- a/internal/auth/plain_separate/plain_separate.go
+++ b/internal/auth/plain_separate/plain_separate.go
@@ -4,11 +4,10 @@ import (
 	"errors"
 
 	"github.com/foxcpp/maddy/internal/config"
 	"github.com/foxcpp/maddy/internal/log"
 	"github.com/foxcpp/maddy/internal/module"
-	"golang.org/x/text/secure/precis"
 )
 
 type Auth struct {
 	modName  string
 	instName string
@@ -52,60 +51,36 @@ func (a *Auth) Init(cfg *config.Map) error {
 	}
 
 	return nil
 }
 
-func (a *Auth) AuthPlain(username, password string) ([]string, error) {
-	key, err := precis.UsernameCaseMapped.CompareKey(username)
-	if err != nil {
-		return nil, err
-	}
-
-	identities := make([]string, 0, 1)
-	if len(a.userTbls) != 0 {
-		for _, tbl := range a.userTbls {
-			repl, ok, err := tbl.Lookup(key)
-			if err != nil {
-				return nil, err
-			}
-			if !ok {
-				continue
-			}
-			if repl != "" {
-				identities = append(identities, repl)
-			} else {
-				identities = append(identities, key)
-			}
-			if a.onlyFirstID && len(identities) != 0 {
-				break
-			}
+func (a *Auth) AuthPlain(username, password string) error {
+	ok := len(a.userTbls) == 0
+	for _, tbl := range a.userTbls {
+		_, tblOk, err := tbl.Lookup(username)
+		if err != nil {
+			return err
 		}
-		if len(identities) == 0 {
-			return nil, errors.New("plain_separate: unknown credentials")
+		if tblOk {
+			ok = true
+			break
 		}
 	}
+	if !ok {
+		return errors.New("user not found in tables")
+	}
 
-	var (
-		lastErr error
-		ok      bool
-	)
-	for _, pass := range a.passwd {
-		passIDs, err := pass.AuthPlain(username, password)
-		if err != nil {
+	var lastErr error
+	for _, p := range a.passwd {
+		if err := p.AuthPlain(username, password); err != nil {
 			lastErr = err
 			continue
 		}
-		if len(a.userTbls) == 0 {
-			identities = append(identities, passIDs...)
-		}
-		ok = true
-	}
-	if !ok {
-		return nil, lastErr
-	}
 
-	return identities, nil
+		return nil
+	}
+	return lastErr
 }
 
 func init() {
 	module.Register("plain_separate", NewAuth)
 }
diff --git a/internal/auth/plain_separate/plain_separate_test.go b/internal/auth/plain_separate/plain_separate_test.go
index 0ffbea0..b8a622d 100644
--- a/internal/auth/plain_separate/plain_separate_test.go
+++ b/internal/auth/plain_separate/plain_separate_test.go
@@ -1,30 +1,29 @@
 package plain_separate
 
 import (
 	"errors"
-	"reflect"
 	"testing"
 
 	"github.com/emersion/go-sasl"
 	"github.com/foxcpp/maddy/internal/module"
 )
 
 type mockAuth struct {
-	db map[string][]string
+	db map[string]bool
 }
 
 func (mockAuth) SASLMechanisms() []string {
 	return []string{sasl.Plain, sasl.Login}
 }
 
-func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
-	ids, ok := m.db[username]
+func (m mockAuth) AuthPlain(username, _ string) error {
+	ok := m.db[username]
 	if !ok {
-		return nil, errors.New("invalid creds")
+		return errors.New("invalid creds")
 	}
-	return ids, nil
+	return nil
 }
 
 type mockTable struct {
 	db map[string]string
 }
@@ -36,114 +35,102 @@ func (m mockTable) Lookup(a string) (string, bool, error) {
 
 func TestPlainSplit_NoUser(t *testing.T) {
 	a := Auth{
 		passwd: []module.PlainAuth{
 			mockAuth{
-				db: map[string][]string{
-					"user1": []string{"user1a", "user1b"},
+				db: map[string]bool{
+					"user1": true,
 				},
 			},
 		},
 	}
 
-	ids, err := a.AuthPlain("user1", "aaa")
+	err := a.AuthPlain("user1", "aaa")
 	if err != nil {
 		t.Fatal("Unexpected error:", err)
 	}
-	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
-		t.Fatal("Wrong ids returned:", ids)
-	}
 }
 
 func TestPlainSplit_NoUser_MultiPass(t *testing.T) {
 	a := Auth{
 		passwd: []module.PlainAuth{
 			mockAuth{
-				db: map[string][]string{
-					"user2": []string{"user2a", "user2b"},
+				db: map[string]bool{
+					"user2": true,
 				},
 			},
 			mockAuth{
-				db: map[string][]string{
-					"user1": []string{"user1a", "user1b"},
+				db: map[string]bool{
+					"user1": true,
 				},
 			},
 		},
 	}
 
-	ids, err := a.AuthPlain("user1", "aaa")
+	err := a.AuthPlain("user1", "aaa")
 	if err != nil {
 		t.Fatal("Unexpected error:", err)
 	}
-	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
-		t.Fatal("Wrong ids returned:", ids)
-	}
 }
 
 func TestPlainSplit_UserPass(t *testing.T) {
 	a := Auth{
 		userTbls: []module.Table{
 			mockTable{
 				db: map[string]string{
-					"user1": "user2",
+					"user1": "",
 				},
 			},
 		},
 		passwd: []module.PlainAuth{
 			mockAuth{
-				db: map[string][]string{
-					"user2": []string{"user2a", "user2b"},
+				db: map[string]bool{
+					"user2": true,
 				},
 			},
 			mockAuth{
-				db: map[string][]string{
-					"user1": []string{"user1a", "user1b"},
+				db: map[string]bool{
+					"user1": true,
 				},
 			},
 		},
 	}
 
-	ids, err := a.AuthPlain("user1", "aaa")
+	err := a.AuthPlain("user1", "aaa")
 	if err != nil {
 		t.Fatal("Unexpected error:", err)
 	}
-	if !reflect.DeepEqual(ids, []string{"user2"}) {
-		t.Fatal("Wrong ids returned:", ids)
-	}
 }
 
 func TestPlainSplit_MultiUser_Pass(t *testing.T) {
 	a := Auth{
 		userTbls: []module.Table{
 			mockTable{
 				db: map[string]string{
-					"userWH": "user1",
+					"userWH": "",
 				},
 			},
 			mockTable{
 				db: map[string]string{
-					"user1": "user2",
+					"user1": "",
 				},
 			},
 		},
 		passwd: []module.PlainAuth{
 			mockAuth{
-				db: map[string][]string{
-					"user2": []string{"user2a", "user2b"},
+				db: map[string]bool{
+					"user2": true,
 				},
 			},
 			mockAuth{
-				db: map[string][]string{
-					"user1": []string{"user1a", "user1b"},
+				db: map[string]bool{
+					"user1": true,
 				},
 			},
 		},
 	}
 
-	ids, err := a.AuthPlain("user1", "aaa")
+	err := a.AuthPlain("user1", "aaa")
 	if err != nil {
 		t.Fatal("Unexpected error:", err)
 	}
-	if !reflect.DeepEqual(ids, []string{"user2"}) {
-		t.Fatal("Wrong ids returned:", ids)
-	}
 }
diff --git a/internal/auth/sasl.go b/internal/auth/sasl.go
index 5e92e0c..7eecdc1 100644
--- a/internal/auth/sasl.go
+++ b/internal/auth/sasl.go
@@ -8,11 +8,10 @@ import (
 	"github.com/emersion/go-sasl"
 	"github.com/foxcpp/maddy/internal/config"
 	modconfig "github.com/foxcpp/maddy/internal/config/module"
 	"github.com/foxcpp/maddy/internal/log"
 	"github.com/foxcpp/maddy/internal/module"
-	"golang.org/x/text/secure/precis"
 )
 
 var (
 	ErrUnsupportedMech = errors.New("Unsupported SASL mechanism")
 )
@@ -37,115 +36,78 @@ func (s *SASLAuth) SASLMechanisms() []string {
 	}
 
 	return mechs
 }
 
-func (s *SASLAuth) AuthPlain(username, password string) ([]string, error) {
+func (s *SASLAuth) AuthPlain(username, password string) error {
 	if len(s.Plain) == 0 {
-		return nil, ErrUnsupportedMech
+		return ErrUnsupportedMech
 	}
 
 	var lastErr error
-	accounts := make([]string, 0, 1)
 	for _, p := range s.Plain {
-		pAccs, err := p.AuthPlain(username, password)
+		err := p.AuthPlain(username, password)
+		if err == nil {
+			return nil
+		}
 		if err != nil {
 			lastErr = err
 			continue
 		}
-
-		if s.OnlyFirstID {
-			return pAccs, nil
-		}
-		accounts = append(accounts, pAccs...)
 	}
 
-	if len(accounts) == 0 {
-		return nil, fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
-	}
-	return accounts, nil
-}
-
-func filterIdentity(accounts []string, identity string) ([]string, error) {
-	if identity == "" {
-		return accounts, nil
-	}
-
-	matchFound := false
-	for _, acc := range accounts {
-		if precis.UsernameCaseMapped.Compare(acc, identity) {
-			accounts = []string{identity}
-			matchFound = true
-			break
-		}
-	}
-	if !matchFound {
-		return nil, errors.New("auth: invalid credentials")
-	}
-	return accounts, nil
+	return fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
 }
 
 // CreateSASL creates the sasl.Server instance for the corresponding mechanism.
-//
-// successCb will be called with the slice of authorization identities
-// associated with credentials used.
-// If it fails - authentication will fail too.
-func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func([]string) error) sasl.Server {
+func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(identity string) error) sasl.Server {
 	switch mech {
 	case sasl.Plain:
 		return sasl.NewPlainServer(func(identity, username, password string) error {
-			accounts, err := s.AuthPlain(username, password)
-			if err != nil {
-				s.Log.Error("authentication failed", err, "username", username, "identity", identity, "src_ip", remoteAddr)
-				return errors.New("auth: invalid credentials")
+			if identity == "" {
+				identity = username
 			}
-			if len(accounts) == 0 {
-				accounts = []string{username}
-			}
-			accounts, err = filterIdentity(accounts, identity)
+
+			err := s.AuthPlain(username, password)
 			if err != nil {
-				s.Log.Error("not authorized", err, "username", username, "identity", identity, "src_ip", remoteAddr)
+				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
 				return errors.New("auth: invalid credentials")
 			}
 
-			return successCb(accounts)
+			return successCb(identity)
 		})
 	case sasl.Login:
 		return sasl.NewLoginServer(func(username, password string) error {
-			accounts, err := s.AuthPlain(username, password)
+			err := s.AuthPlain(username, password)
 			if err != nil {
 				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
 				return errors.New("auth: invalid credentials")
 			}
 
-			return successCb(accounts)
+			return successCb(username)
 		})
 	}
 	return FailingSASLServ{Err: ErrUnsupportedMech}
 }
 
 // AddProvider adds the SASL authentication provider to its mapping by parsing
 // the 'auth' configuration directive.
 func (s *SASLAuth) AddProvider(m *config.Map, node *config.Node) error {
-	mod, err := modconfig.SASLAuthDirective(m, node)
-	if err != nil {
+	var any interface{}
+	if err := modconfig.ModuleFromNode(node.Args, node, m.Globals, &any); err != nil {
 		return err
 	}
-	saslAuth := mod.(module.SASLProvider)
-	for _, mech := range saslAuth.SASLMechanisms() {
-		switch mech {
-		case sasl.Login, sasl.Plain:
-			plainAuth, ok := saslAuth.(module.PlainAuth)
-			if !ok {
-				return m.MatchErr("auth: provider does not implement PlainAuth even though it reports PLAIN/LOGIN mechanism")
-			}
-			s.Plain = append(s.Plain, plainAuth)
-		default:
-			return m.MatchErr("auth: unknown SASL mechanism")
-		}
+
+	hasAny := false
+	if plainAuth, ok := any.(module.PlainAuth); ok {
+		s.Plain = append(s.Plain, plainAuth)
+		hasAny = true
 	}
 
+	if !hasAny {
+		return m.MatchErr("auth: specified module does not provide any SASL mechanism")
+	}
 	return nil
 }
 
 type FailingSASLServ struct{ Err error }
 
diff --git a/internal/auth/sasl_test.go b/internal/auth/sasl_test.go
index 8837dee..10807cf 100644
--- a/internal/auth/sasl_test.go
+++ b/internal/auth/sasl_test.go
@@ -1,103 +1,71 @@
 package auth
 
 import (
 	"errors"
 	"net"
-	"reflect"
 	"testing"
 
-	"github.com/emersion/go-sasl"
 	"github.com/foxcpp/maddy/internal/module"
 	"github.com/foxcpp/maddy/internal/testutils"
 )
 
 type mockAuth struct {
-	db map[string][]string
+	db map[string]bool
 }
 
-func (mockAuth) SASLMechanisms() []string {
-	return []string{sasl.Plain, sasl.Login}
-}
-
-func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
-	ids, ok := m.db[username]
+func (m mockAuth) AuthPlain(username, _ string) error {
+	ok := m.db[username]
 	if !ok {
-		return nil, errors.New("invalid creds")
+		return errors.New("invalid creds")
 	}
-	return ids, nil
+	return nil
 }
 
 func TestCreateSASL(t *testing.T) {
 	a := SASLAuth{
 		Log: testutils.Logger(t, "saslauth"),
 		Plain: []module.PlainAuth{
 			&mockAuth{
-				db: map[string][]string{
-					"user1": []string{"user1a", "user1b"},
+				db: map[string]bool{
+					"user1": true,
 				},
 			},
 		},
 	}
 
 	t.Run("XWHATEVER", func(t *testing.T) {
-		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func([]string) error { return nil })
+		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func(string) error { return nil })
 		_, _, err := srv.Next([]byte(""))
 		if err == nil {
 			t.Error("No error for XWHATEVER use")
 		}
 	})
 
 	t.Run("PLAIN", func(t *testing.T) {
-		var ids []string
-		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
-			ids = passed
+		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
+			if id != "user1" {
+				t.Fatal("Wrong auth. identities passed to callback:", id)
+			}
 			return nil
 		})
 
 		_, _, err := srv.Next([]byte("\x00user1\x00aa"))
 		if err != nil {
 			t.Error("Unexpected error:", err)
 		}
-		if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
-			t.Error("Wrong auth. identities passed to callback:", ids)
-		}
 	})
 
-	t.Run("PLAIN with autorization identity", func(t *testing.T) {
-		var ids []string
-		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
-			ids = passed
+	t.Run("PLAIN with authorization identity", func(t *testing.T) {
+		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
+			if id != "user1a" {
+				t.Fatal("Wrong authorization identity passed:", id)
+			}
 			return nil
 		})
 
 		_, _, err := srv.Next([]byte("user1a\x00user1\x00aa"))
 		if err != nil {
 			t.Error("Unexpected error:", err)
 		}
-		if !reflect.DeepEqual(ids, []string{"user1a"}) {
-			t.Error("Wrong auth. identities passed to callback:", ids)
-		}
-	})
-
-	t.Run("PLAIN with wrong authorization identity", func(t *testing.T) {
-		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
-			return nil
-		})
-
-		_, _, err := srv.Next([]byte("user1c\x00user1\x00aa"))
-		if err == nil {
-			t.Error("Next should fail")
-		}
-	})
-
-	t.Run("PLAIN with wrong authentication identity", func(t *testing.T) {
-		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
-			return nil
-		})
-
-		_, _, err := srv.Next([]byte("\x00user2\x00aa"))
-		if err == nil {
-			t.Error("Next should fail")
-		}
 	})
 }
diff --git a/internal/auth/shadow/module.go b/internal/auth/shadow/module.go
index 5812b70..6135a03 100644
--- a/internal/auth/shadow/module.go
+++ b/internal/auth/shadow/module.go
@@ -64,36 +64,36 @@ func (a *Auth) Init(cfg *config.Map) error {
 	}
 
 	return nil
 }
 
-func (a *Auth) AuthPlain(username, password string) ([]string, error) {
+func (a *Auth) AuthPlain(username, password string) error {
 	if a.useHelper {
-		return []string{username}, external.AuthUsingHelper(a.helperPath, username, password)
+		return external.AuthUsingHelper(a.helperPath, username, password)
 	}
 
 	ent, err := Lookup(username)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	if !ent.IsAccountValid() {
-		return nil, fmt.Errorf("shadow: account is expired")
+		return fmt.Errorf("shadow: account is expired")
 	}
 
 	if !ent.IsPasswordValid() {
-		return nil, fmt.Errorf("shadow: password is expired")
+		return fmt.Errorf("shadow: password is expired")
 	}
 
 	if err := ent.VerifyPassword(password); err != nil {
 		if err == ErrWrongPassword {
-			return nil, module.ErrUnknownCredentials
+			return module.ErrUnknownCredentials
 		}
-		return nil, err
+		return err
 	}
 
-	return []string{username}, nil
+	return nil
 }
 
 func init() {
 	module.Register("shadow", New)
 }
diff --git a/internal/config/module/interfaces.go b/internal/config/module/interfaces.go
index cf8154c..7ea1a30 100644
--- a/internal/config/module/interfaces.go
+++ b/internal/config/module/interfaces.go
@@ -49,18 +49,10 @@ func StorageDirective(m *config.Map, node *config.Node) (interface{}, error) {
 		return nil, err
 	}
 	return backend, nil
 }
 
-func SASLAuthDirective(m *config.Map, node *config.Node) (interface{}, error) {
-	var provider module.SASLProvider
-	if err := ModuleFromNode(node.Args, node, m.Globals, &provider); err != nil {
-		return nil, err
-	}
-	return provider, nil
-}
-
 func TableDirective(m *config.Map, node *config.Node) (interface{}, error) {
 	var tbl module.Table
 	if err := ModuleFromNode(node.Args, node, m.Globals, &tbl); err != nil {
 		return nil, err
 	}
diff --git a/internal/endpoint/imap/imap.go b/internal/endpoint/imap/imap.go
index 808cee2..91a7796 100644
--- a/internal/endpoint/imap/imap.go
+++ b/internal/endpoint/imap/imap.go
@@ -121,12 +121,12 @@ func (endp *Endpoint) Init(cfg *config.Map) error {
 		return err
 	}
 
 	for _, mech := range endp.saslAuth.SASLMechanisms() {
 		endp.serv.EnableAuth(mech, func(c imapserver.Conn) sasl.Server {
-			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(ids []string) error {
-				return endp.openAccount(c, ids)
+			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string) error {
+				return endp.openAccount(c, identity)
 			})
 		})
 	}
 
 	if err := endp.setupListeners(addresses); err != nil {
@@ -197,30 +197,28 @@ func (endp *Endpoint) Close() error {
 	}
 	endp.listenersWg.Wait()
 	return nil
 }
 
-func (endp *Endpoint) openAccount(c imapserver.Conn, identities []string) error {
-	u, err := endp.Store.GetOrCreateUser(identities[0])
+func (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {
+	u, err := endp.Store.GetOrCreateUser(identity)
 	if err != nil {
 		return err
 	}
 	ctx := c.Context()
 	ctx.State = imap.AuthenticatedState
 	ctx.User = u
 	return nil
 }
 
 func (endp *Endpoint) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {
-	_, err := endp.saslAuth.AuthPlain(username, password)
+	err := endp.saslAuth.AuthPlain(username, password)
 	if err != nil {
 		endp.Log.Error("authentication failed", err, "username", username, "src_ip", connInfo.RemoteAddr)
 		return nil, imapbackend.ErrInvalidCredentials
 	}
 
-	// TODO: Wrap GetOrCreateUser and possibly implement INBOXES extension
-	// (though it is draft 00 for quite some time so it likely has no future).
 	return endp.Store.GetOrCreateUser(username)
 }
 
 func (endp *Endpoint) EnableChildrenExt() bool {
 	return endp.Store.(children.Backend).EnableChildrenExt()
diff --git a/internal/endpoint/smtp/smtp.go b/internal/endpoint/smtp/smtp.go
index 3b71f5e..e3ceb27 100644
--- a/internal/endpoint/smtp/smtp.go
+++ b/internal/endpoint/smtp/smtp.go
@@ -267,12 +267,12 @@ func (endp *Endpoint) setConfig(cfg *config.Map) error {
 			state := c.State()
 			if err := endp.pipeline.RunEarlyChecks(context.TODO(), &state); err != nil {
 				return auth.FailingSASLServ{Err: endp.wrapErr("", true, err)}
 			}
 
-			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(ids []string) error {
-				c.SetSession(endp.newSession(false, ids[0], "", &state))
+			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(id string) error {
+				c.SetSession(endp.newSession(false, id, "", &state))
 				return nil
 			})
 		})
 	}
 
@@ -330,18 +330,17 @@ func (endp *Endpoint) Login(state *smtp.ConnectionState, username, password stri
 	// Executed before authentication and session initialization.
 	if err := endp.pipeline.RunEarlyChecks(context.TODO(), state); err != nil {
 		return nil, endp.wrapErr("", true, err)
 	}
 
-	_, err := endp.saslAuth.AuthPlain(username, password)
+	err := endp.saslAuth.AuthPlain(username, password)
 	if err != nil {
 		// TODO: Update fail2ban filters.
 		endp.Log.Error("authentication failed", err, "username", username, "src_ip", state.RemoteAddr)
 		return nil, errors.New("Invalid credentials")
 	}
 
-	// TODO: Pass valid identifies to SMTP pipeline.
 	return endp.newSession(false, username, password, state), nil
 }
 
 func (endp *Endpoint) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
 	if endp.authAlwaysRequired {
diff --git a/internal/module/auth.go b/internal/module/auth.go
index 06a58bc..3daaab7 100644
--- a/internal/module/auth.go
+++ b/internal/module/auth.go
@@ -10,25 +10,7 @@ var (
 )
 
 // PlainAuth is the interface implemented by modules providing authentication using
 // username:password pairs.
 type PlainAuth interface {
-	AuthPlain(username, password string) ([]string, error)
-}
-
-// SASLProvider is the interface implemented by modules and used by protocol
-// endpoints that rely on SASL framework for user authentication.
-//
-// This actual interface is only used to indicate that the module is a
-// SASL-compatible auth. provider. For each unique value returned by
-// SASLMechanisms, the module object should also implement the coresponding
-// mechanism-specific interface.
-//
-// *Rationale*: There is no single generic interface that would handle any SASL
-// mechanism while permiting the use of a credentials set estabilished once with
-// multiple auth. providers at once.
-//
-// Per-mechanism interfaces:
-// - PLAIN => PlainAuth
-type SASLProvider interface {
-	SASLMechanisms() []string
+	AuthPlain(username, password string) error
 }
diff --git a/internal/module/dummy.go b/internal/module/dummy.go
index ee93003..180bff5 100644
--- a/internal/module/dummy.go
+++ b/internal/module/dummy.go
@@ -2,11 +2,10 @@ package module
 
 import (
 	"context"
 
 	"github.com/emersion/go-message/textproto"
-	"github.com/emersion/go-sasl"
 	"github.com/foxcpp/maddy/internal/buffer"
 	"github.com/foxcpp/maddy/internal/config"
 )
 
 // Dummy is a struct that implements PlainAuth and DeliveryTarget
@@ -14,16 +13,12 @@ import (
 //
 // It is always registered under the 'dummy' name and can be used in both tests
 // and the actual server code (but the latter is kinda pointless).
 type Dummy struct{ instName string }
 
-func (d *Dummy) SASLMechanisms() []string {
-	return []string{sasl.Plain, sasl.Login}
-}
-
-func (d *Dummy) AuthPlain(username, _ string) ([]string, error) {
-	return []string{username}, nil
+func (d *Dummy) AuthPlain(username, _ string) error {
+	return nil
 }
 
 func (d *Dummy) Name() string {
 	return "dummy"
 }
diff --git a/internal/storage/sql/sql.go b/internal/storage/sql/sql.go
index 435ff3b..a453a56 100644
--- a/internal/storage/sql/sql.go
+++ b/internal/storage/sql/sql.go
@@ -418,29 +418,29 @@ func prepareUsername(username string) (string, error) {
 	}
 
 	return mbox + "@" + domain, nil
 }
 
-func (store *Storage) AuthPlain(username, password string) ([]string, error) {
+func (store *Storage) AuthPlain(username, password string) error {
 	// TODO: Pass session context there.
 	defer trace.StartRegion(context.Background(), "sql/AuthPlain").End()
 
 	accountName, err := prepareUsername(username)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	password, err = precis.OpaqueString.CompareKey(password)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	// TODO: Make go-imap-sql CheckPlain return an actual error.
 	if !store.Back.CheckPlain(accountName, password) {
-		return nil, module.ErrUnknownCredentials
+		return module.ErrUnknownCredentials
 	}
-	return []string{username}, nil
+	return nil
 }
 
 func (store *Storage) GetOrCreateUser(username string) (backend.User, error) {
 	accountName, err := prepareUsername(username)
 	if err != nil {

Vulnerable code: The authorization identity is required to be a valid RFC 5321 e-mail address.
It is returned as the authorization identity.
The authorization identtity is the same as authorization identity.

The authorization identtity is the same as authorization identity.

	expect_address yes
TL;DR (assuming you have maddy group):
The authorization identtity is the same as authorization identity.

   First match wins same as in step 2.

The set of authorization identities estabilished by plain_separate is the list of
strings found in 'user' tables. If there are no 'user' tables (only 'pass'
directives are specified), the list of authorization identities returned by
auth. providers is used.
*Note*: If a username table returns empty string, the authorization identity is
assumed to be the same as authentication identity.
func (ea *ExternalAuth) AuthPlain(username, password string) ([]string, error) {
		return nil, module.ErrUnknownCredentials
	// TODO: Extend process protocol to support multiple authorization identities.
	return []string{username}, AuthUsingHelper(ea.helperPath, accountName, password)
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
			return nil, err
		return nil, err
	return []string{username}, nil
	"golang.org/x/text/secure/precis"
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
	key, err := precis.UsernameCaseMapped.CompareKey(username)
	if err != nil {
		return nil, err
	}

	identities := make([]string, 0, 1)
	if len(a.userTbls) != 0 {
		for _, tbl := range a.userTbls {
			repl, ok, err := tbl.Lookup(key)
			if err != nil {
				return nil, err
			}
			if !ok {
				continue
			}
			if repl != "" {
				identities = append(identities, repl)
			} else {
				identities = append(identities, key)
			}
			if a.onlyFirstID && len(identities) != 0 {
				break
			}
		if len(identities) == 0 {
			return nil, errors.New("plain_separate: unknown credentials")
	var (
		lastErr error
		ok      bool
	)
	for _, pass := range a.passwd {
		passIDs, err := pass.AuthPlain(username, password)
		if err != nil {
		if len(a.userTbls) == 0 {
			identities = append(identities, passIDs...)
		}
		ok = true
	}
	if !ok {
		return nil, lastErr
	}
	return identities, nil
	"reflect"
	db map[string][]string
func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
	ids, ok := m.db[username]
		return nil, errors.New("invalid creds")
	return ids, nil
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
					"user1": "user2",
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user2"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
					"userWH": "user1",
					"user1": "user2",
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user2"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
	"golang.org/x/text/secure/precis"
func (s *SASLAuth) AuthPlain(username, password string) ([]string, error) {
		return nil, ErrUnsupportedMech
	accounts := make([]string, 0, 1)
		pAccs, err := p.AuthPlain(username, password)

		if s.OnlyFirstID {
			return pAccs, nil
		}
		accounts = append(accounts, pAccs...)
	if len(accounts) == 0 {
		return nil, fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
	}
	return accounts, nil
}

func filterIdentity(accounts []string, identity string) ([]string, error) {
	if identity == "" {
		return accounts, nil
	}

	matchFound := false
	for _, acc := range accounts {
		if precis.UsernameCaseMapped.Compare(acc, identity) {
			accounts = []string{identity}
			matchFound = true
			break
		}
	}
	if !matchFound {
		return nil, errors.New("auth: invalid credentials")
	}
	return accounts, nil
//
// successCb will be called with the slice of authorization identities
// associated with credentials used.
// If it fails - authentication will fail too.
func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func([]string) error) sasl.Server {
			accounts, err := s.AuthPlain(username, password)
			if err != nil {
				s.Log.Error("authentication failed", err, "username", username, "identity", identity, "src_ip", remoteAddr)
				return errors.New("auth: invalid credentials")
			if len(accounts) == 0 {
				accounts = []string{username}
			}
			accounts, err = filterIdentity(accounts, identity)
				s.Log.Error("not authorized", err, "username", username, "identity", identity, "src_ip", remoteAddr)
			return successCb(accounts)
			accounts, err := s.AuthPlain(username, password)
			return successCb(accounts)
	mod, err := modconfig.SASLAuthDirective(m, node)
	if err != nil {
	saslAuth := mod.(module.SASLProvider)
	for _, mech := range saslAuth.SASLMechanisms() {
		switch mech {
		case sasl.Login, sasl.Plain:
			plainAuth, ok := saslAuth.(module.PlainAuth)
			if !ok {
				return m.MatchErr("auth: provider does not implement PlainAuth even though it reports PLAIN/LOGIN mechanism")
			}
			s.Plain = append(s.Plain, plainAuth)
		default:
			return m.MatchErr("auth: unknown SASL mechanism")
		}
	"reflect"
	"github.com/emersion/go-sasl"
	db map[string][]string
func (mockAuth) SASLMechanisms() []string {
	return []string{sasl.Plain, sasl.Login}
}

func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
	ids, ok := m.db[username]
		return nil, errors.New("invalid creds")
	return ids, nil
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func([]string) error { return nil })
		var ids []string
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			ids = passed
		if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
			t.Error("Wrong auth. identities passed to callback:", ids)
		}
	t.Run("PLAIN with autorization identity", func(t *testing.T) {
		var ids []string
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			ids = passed
		if !reflect.DeepEqual(ids, []string{"user1a"}) {
			t.Error("Wrong auth. identities passed to callback:", ids)
		}
	})

	t.Run("PLAIN with wrong authorization identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			return nil
		})

		_, _, err := srv.Next([]byte("user1c\x00user1\x00aa"))
		if err == nil {
			t.Error("Next should fail")
		}
	})

	t.Run("PLAIN with wrong authentication identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			return nil
		})

		_, _, err := srv.Next([]byte("\x00user2\x00aa"))
		if err == nil {
			t.Error("Next should fail")
		}
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
		return []string{username}, external.AuthUsingHelper(a.helperPath, username, password)
		return nil, err
		return nil, fmt.Errorf("shadow: account is expired")
		return nil, fmt.Errorf("shadow: password is expired")
			return nil, module.ErrUnknownCredentials
		return nil, err
	return []string{username}, nil
func SASLAuthDirective(m *config.Map, node *config.Node) (interface{}, error) {
	var provider module.SASLProvider
	if err := ModuleFromNode(node.Args, node, m.Globals, &provider); err != nil {
		return nil, err
	}
	return provider, nil
}

			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(ids []string) error {
				return endp.openAccount(c, ids)
func (endp *Endpoint) openAccount(c imapserver.Conn, identities []string) error {
	u, err := endp.Store.GetOrCreateUser(identities[0])
	_, err := endp.saslAuth.AuthPlain(username, password)
	// TODO: Wrap GetOrCreateUser and possibly implement INBOXES extension
	// (though it is draft 00 for quite some time so it likely has no future).
			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(ids []string) error {
				c.SetSession(endp.newSession(false, ids[0], "", &state))
	_, err := endp.saslAuth.AuthPlain(username, password)
	// TODO: Pass valid identifies to SMTP pipeline.
	AuthPlain(username, password string) ([]string, error)
}

// SASLProvider is the interface implemented by modules and used by protocol
// endpoints that rely on SASL framework for user authentication.
//
// This actual interface is only used to indicate that the module is a
// SASL-compatible auth. provider. For each unique value returned by
// SASLMechanisms, the module object should also implement the coresponding
// mechanism-specific interface.
//
// *Rationale*: There is no single generic interface that would handle any SASL
// mechanism while permiting the use of a credentials set estabilished once with
// multiple auth. providers at once.
//
// Per-mechanism interfaces:
// - PLAIN => PlainAuth
type SASLProvider interface {
	SASLMechanisms() []string
	"github.com/emersion/go-sasl"
func (d *Dummy) SASLMechanisms() []string {
	return []string{sasl.Plain, sasl.Login}
}

func (d *Dummy) AuthPlain(username, _ string) ([]string, error) {
	return []string{username}, nil
func (store *Storage) AuthPlain(username, password string) ([]string, error) {
		return nil, err
		return nil, err
		return nil, module.ErrUnknownCredentials
	return []string{username}, nil
Fixed code: The username is required to be a valid RFC 5321 e-mail address.
TL;DR (assuming you have the maddy group):
   If authentication with all providers fails - an error is returned.
func (ea *ExternalAuth) AuthPlain(username, password string) error {
		return module.ErrUnknownCredentials
	return AuthUsingHelper(ea.helperPath, accountName, password)
func (a *Auth) AuthPlain(username, password string) error {
			return err
		return err
	return nil
func (a *Auth) AuthPlain(username, password string) error {
	ok := len(a.userTbls) == 0
	for _, tbl := range a.userTbls {
		_, tblOk, err := tbl.Lookup(username)
		if err != nil {
			return err
		if tblOk {
			ok = true
			break
	if !ok {
		return errors.New("user not found in tables")
	}
	var lastErr error
	for _, p := range a.passwd {
		if err := p.AuthPlain(username, password); err != nil {
		return nil
	}
	return lastErr
	db map[string]bool
func (m mockAuth) AuthPlain(username, _ string) error {
	ok := m.db[username]
		return errors.New("invalid creds")
	return nil
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
					"user1": "",
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
					"userWH": "",
					"user1": "",
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
func (s *SASLAuth) AuthPlain(username, password string) error {
		return ErrUnsupportedMech
		err := p.AuthPlain(username, password)
		if err == nil {
			return nil
		}
	return fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(identity string) error) sasl.Server {
			if identity == "" {
				identity = username

			err := s.AuthPlain(username, password)
				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
			return successCb(identity)
			err := s.AuthPlain(username, password)
			return successCb(username)
	var any interface{}
	if err := modconfig.ModuleFromNode(node.Args, node, m.Globals, &any); err != nil {

	hasAny := false
	if plainAuth, ok := any.(module.PlainAuth); ok {
		s.Plain = append(s.Plain, plainAuth)
		hasAny = true
	if !hasAny {
		return m.MatchErr("auth: specified module does not provide any SASL mechanism")
	}
	db map[string]bool
func (m mockAuth) AuthPlain(username, _ string) error {
	ok := m.db[username]
		return errors.New("invalid creds")
	return nil
				db: map[string]bool{
					"user1": true,
		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func(string) error { return nil })
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
			if id != "user1" {
				t.Fatal("Wrong auth. identities passed to callback:", id)
			}
	t.Run("PLAIN with authorization identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
			if id != "user1a" {
				t.Fatal("Wrong authorization identity passed:", id)
			}
func (a *Auth) AuthPlain(username, password string) error {
		return external.AuthUsingHelper(a.helperPath, username, password)
		return err
		return fmt.Errorf("shadow: account is expired")
		return fmt.Errorf("shadow: password is expired")
			return module.ErrUnknownCredentials
		return err
	return nil
			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string) error {
				return endp.openAccount(c, identity)
func (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {
	u, err := endp.Store.GetOrCreateUser(identity)
	err := endp.saslAuth.AuthPlain(username, password)
			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(id string) error {
				c.SetSession(endp.newSession(false, id, "", &state))
	err := endp.saslAuth.AuthPlain(username, password)
	AuthPlain(username, password string) error
func (d *Dummy) AuthPlain(username, _ string) error {
	return nil
func (store *Storage) AuthPlain(username, password string) error {
		return err
		return err
		return module.ErrUnknownCredentials
	return nil