From ac9117dc500ef9057373f019d2b6deeaa4ac61cc Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 14:29:34 +0100 Subject: [PATCH 1/4] Add SMTP authentication auto-discovery Implemented a mechanism to automatically discover and select the strongest supported SMTP authentication type. This feature simplifies the authentication process for users and enhances security by prioritizing stronger mechanisms based on server capabilities. Corresponding tests and documentation have been updated. --- auth.go | 20 ++++++++++++++++++++ client.go | 36 +++++++++++++++++++++++++++++++++++- client_test.go | 5 +++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/auth.go b/auth.go index 66254ee..e4adfe6 100644 --- a/auth.go +++ b/auth.go @@ -136,6 +136,21 @@ const ( // // https://datatracker.ietf.org/doc/html/rfc7677 SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" + + // SMTPAuthAutoDiscover is a mechanism that dynamically discovers all authentication mechanisms + // supported by the SMTP server and selects the strongest available one. + // + // This type simplifies authentication by automatically negotiating the most secure mechanism + // offered by the server, based on a predefined security ranking. For instance, mechanisms like + // SCRAM-SHA-256(-PLUS) or XOAUTH2 are prioritized over weaker mechanisms such as CRAM-MD5 or PLAIN. + // + // The negotiation process ensures that mechanisms requiring additional capabilities (e.g., + // SCRAM-SHA-X-PLUS with TLS channel binding) are only selected when the necessary prerequisites + // are in place, such as an active TLS-secured connection. + // + // By automating mechanism selection, SMTPAuthAutoDiscover minimizes configuration effort while + // maximizing security and compatibility with a wide range of SMTP servers. + SMTPAuthAutoDiscover SMTPAuthType = "AUTODISCOVER" ) // SMTP Auth related static errors @@ -170,6 +185,11 @@ var ( // ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP // authentication type. ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") + + // ErrNoSupportedAuthDiscovered is returned when the SMTP Auth AutoDiscover process fails to identify + // any supported authentication mechanisms offered by the server. + ErrNoSupportedAuthDiscovered = errors.New("SMTP Auth autodiscover was not able to detect a supported " + + "authentication mechanism") ) // UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type diff --git a/client.go b/client.go index 9b3251e..f92c991 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,16 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - switch c.smtpAuthType { + authType := c.smtpAuthType + if c.smtpAuthType == SMTPAuthAutoDiscover { + discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) + if err != nil { + return err + } + authType = discoveredType + } + + switch authType { case SMTPAuthPlain: if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { return ErrPlainAuthNotSupported @@ -1172,6 +1181,31 @@ func (c *Client) auth() error { return nil } +func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { + preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, + SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} + if !c.isEncrypted { + preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5} + } + mechs := strings.Split(supported, " ") + + for _, item := range preferList { + if sliceContains(mechs, string(item)) { + return item, nil + } + } + return "", ErrNoSupportedAuthDiscovered +} + +func sliceContains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + // sendSingleMsg sends out a single message and returns an error if the transmission or // delivery fails. It is invoked by the public Send methods. // diff --git a/client_test.go b/client_test.go index 2ac8811..5138c17 100644 --- a/client_test.go +++ b/client_test.go @@ -2304,6 +2304,11 @@ func TestClient_auth(t *testing.T) { name string authType SMTPAuthType }{ + {"LOGIN via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"PLAIN via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"SCRAM-SHA-1 via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"SCRAM-SHA-256 via AUTODISCOVER", SMTPAuthAutoDiscover}, + {"XOAUTH2 via AUTODISCOVER", SMTPAuthAutoDiscover}, {"CRAM-MD5", SMTPAuthCramMD5}, {"LOGIN", SMTPAuthLogin}, {"LOGIN-NOENC", SMTPAuthLoginNoEnc}, From 6d3640a16684765da23052ee7a2e264a70db5992 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:38:29 +0100 Subject: [PATCH 2/4] Fix auth type auto-discovery and add test cases Refactor the auth type initialization to prevent incorrect assignments and handle empty supported lists. Added comprehensive test cases to verify auto-discovery selection of the strongest authentication method and ensure robustness against empty or invalid input. --- client.go | 5 ++++- client_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index f92c991..e61bfad 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,7 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - authType := c.smtpAuthType + var authType SMTPAuthType if c.smtpAuthType == SMTPAuthAutoDiscover { discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) if err != nil { @@ -1182,6 +1182,9 @@ func (c *Client) auth() error { } func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { + if supported == "" { + return "", ErrNoSupportedAuthDiscovered + } preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} if !c.isEncrypted { diff --git a/client_test.go b/client_test.go index 5138c17..97289b8 100644 --- a/client_test.go +++ b/client_test.go @@ -2514,6 +2514,42 @@ func TestClient_auth(t *testing.T) { }) } +func TestClient_authTypeAutoDiscover(t *testing.T) { + tests := []struct { + supported string + tls bool + expect SMTPAuthType + shouldFail bool + }{ + {"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA256PLUS, false}, + {"LOGIN SCRAM-SHA-256 SCRAM-SHA-1 SCRAM-SHA-256-PLUS SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA256, false}, + {"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", true, SMTPAuthSCRAMSHA1PLUS, false}, + {"LOGIN PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS", false, SMTPAuthSCRAMSHA1, false}, + {"LOGIN XOAUTH2 SCRAM-SHA-1-PLUS", false, SMTPAuthXOAUTH2, false}, + {"PLAIN LOGIN CRAM-MD5", false, SMTPAuthCramMD5, false}, + {"CRAM-MD5", false, SMTPAuthCramMD5, false}, + {"PLAIN", true, SMTPAuthPlain, false}, + {"LOGIN PLAIN", true, SMTPAuthPlain, false}, + {"LOGIN PLAIN", false, "no secure mechanism", true}, + {"", false, "supported list empty", true}, + } + for _, tt := range tests { + t.Run("AutoDiscover selects the strongest auth type: "+string(tt.expect), func(t *testing.T) { + client := &Client{smtpAuthType: SMTPAuthAutoDiscover, isEncrypted: tt.tls} + authType, err := client.authTypeAutoDiscover(tt.supported) + if err != nil && !tt.shouldFail { + t.Fatalf("failed to auto discover auth type: %s", err) + } + if tt.shouldFail && err == nil { + t.Fatal("expected auto discover to fail") + } + if !tt.shouldFail && authType != tt.expect { + t.Errorf("expected strongest auth type: %s, got: %s", tt.expect, authType) + } + }) + } +} + func TestClient_Send(t *testing.T) { message := testMessage(t) t.Run("connect and send email", func(t *testing.T) { From 427e8fd1ed9e7861f7389aac073613f01694643d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:41:32 +0100 Subject: [PATCH 3/4] Format code block consistently Refactor the `preferList` definition in `client.go` for improved readability and consistency. This change ensures the code aligns with standard formatting practices. --- client.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index e61bfad..a611d4c 100644 --- a/client.go +++ b/client.go @@ -1185,8 +1185,10 @@ func (c *Client) authTypeAutoDiscover(supported string) (SMTPAuthType, error) { if supported == "" { return "", ErrNoSupportedAuthDiscovered } - preferList := []SMTPAuthType{SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, - SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin} + preferList := []SMTPAuthType{ + SMTPAuthSCRAMSHA256PLUS, SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1PLUS, SMTPAuthSCRAMSHA1, + SMTPAuthXOAUTH2, SMTPAuthCramMD5, SMTPAuthPlain, SMTPAuthLogin, + } if !c.isEncrypted { preferList = []SMTPAuthType{SMTPAuthSCRAMSHA256, SMTPAuthSCRAMSHA1, SMTPAuthXOAUTH2, SMTPAuthCramMD5} } From d8df26bbc88536dc2afcab5ab438b12a6eb32627 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Sat, 16 Nov 2024 21:46:46 +0100 Subject: [PATCH 4/4] Fix regression --- client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client.go b/client.go index a611d4c..c301f45 100644 --- a/client.go +++ b/client.go @@ -1100,7 +1100,7 @@ func (c *Client) auth() error { return fmt.Errorf("server does not support SMTP AUTH") } - var authType SMTPAuthType + authType := c.smtpAuthType if c.smtpAuthType == SMTPAuthAutoDiscover { discoveredType, err := c.authTypeAutoDiscover(smtpAuthType) if err != nil {