Merge pull request #373 from wneessen/feature/smtp-auth-autoselect

Add SMTP authentication auto-discovery
This commit is contained in:
Winni Neessen 2024-11-16 21:51:23 +01:00 committed by GitHub
commit c37ed7c723
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 101 additions and 1 deletions

20
auth.go
View file

@ -136,6 +136,21 @@ const (
// //
// https://datatracker.ietf.org/doc/html/rfc7677 // https://datatracker.ietf.org/doc/html/rfc7677
SMTPAuthSCRAMSHA256PLUS SMTPAuthType = "SCRAM-SHA-256-PLUS" 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 // 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 // ErrSCRAMSHA256PLUSAuthNotSupported is returned when the server does not support the "SCRAM-SHA-256-PLUS" SMTP
// authentication type. // authentication type.
ErrSCRAMSHA256PLUSAuthNotSupported = errors.New("server does not support SMTP AUTH type: SCRAM-SHA-256-PLUS") 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 // UnmarshalString satisfies the fig.StringUnmarshaler interface for the SMTPAuthType type

View file

@ -1100,7 +1100,16 @@ func (c *Client) auth() error {
return fmt.Errorf("server does not support SMTP AUTH") 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: case SMTPAuthPlain:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported return ErrPlainAuthNotSupported
@ -1172,6 +1181,36 @@ func (c *Client) auth() error {
return nil return nil
} }
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 {
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 // sendSingleMsg sends out a single message and returns an error if the transmission or
// delivery fails. It is invoked by the public Send methods. // delivery fails. It is invoked by the public Send methods.
// //

View file

@ -2304,6 +2304,11 @@ func TestClient_auth(t *testing.T) {
name string name string
authType SMTPAuthType 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}, {"CRAM-MD5", SMTPAuthCramMD5},
{"LOGIN", SMTPAuthLogin}, {"LOGIN", SMTPAuthLogin},
{"LOGIN-NOENC", SMTPAuthLoginNoEnc}, {"LOGIN-NOENC", SMTPAuthLoginNoEnc},
@ -2509,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) { func TestClient_Send(t *testing.T) {
message := testMessage(t) message := testMessage(t)
t.Run("connect and send email", func(t *testing.T) { t.Run("connect and send email", func(t *testing.T) {