Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow unencrypted PLAIN and LOGIN smtp authentication #344

Merged
merged 7 commits into from
Oct 22, 2024
Merged
37 changes: 37 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ const (
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
SMTPAuthLogin SMTPAuthType = "LOGIN"

// SMTPAuthLoginNoEnc is the "LOGIN" SASL authentication mechanism. This authentication mechanism
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
// automatically matches the MS spec.
//
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
// plaintext over the internet connection, by default we only allow this mechanism over
// a TLS secured connection. This authentiation mechanism overrides this default and will
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
// connection has already been secured in a different way (e. g. a SSH tunnel)
//
// Note: Use this authentication method with caution. If used in the wrong way, you might
// expose your authentication information over unencrypted channels!
//
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
//
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"

// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
// option and should not be used. Instead, for mail servers that do no support/require
// authentication, the Client should not be passed the WithSMTPAuth option at all.
Expand All @@ -62,6 +81,20 @@ const (
// https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlain SMTPAuthType = "PLAIN"

// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
//
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
// plaintext over the internet connection, by default we only allow this mechanism over
// a TLS secured connection. This authentiation mechanism overrides this default and will
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
// connection has already been secured in a different way (e. g. a SSH tunnel)
//
// Note: Use this authentication method with caution. If used in the wrong way, you might
// expose your authentication information over unencrypted channels!
//
// https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"

// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
Expand Down Expand Up @@ -149,10 +182,14 @@ func (sa *SMTPAuthType) UnmarshalString(value string) error {
*sa = SMTPAuthCustom
case "login":
*sa = SMTPAuthLogin
case "login-noenc":
*sa = SMTPAuthLoginNoEnc
case "none", "noauth", "no":
*sa = SMTPAuthNoAuth
case "plain":
*sa = SMTPAuthPlain
case "plain-noenc":
*sa = SMTPAuthPlainNoEnc
case "scram-sha-1", "scram-sha1", "scramsha1":
*sa = SMTPAuthSCRAMSHA1
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
Expand Down
2 changes: 2 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ func TestSMTPAuthType_UnmarshalString(t *testing.T) {
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
{"CUSTOM", "custom", SMTPAuthCustom},
{"LOGIN", "login", SMTPAuthLogin},
{"LOGIN-NOENC", "login-noenc", SMTPAuthLoginNoEnc},
{"NONE: none", "none", SMTPAuthNoAuth},
{"NONE: noauth", "noauth", SMTPAuthNoAuth},
{"NONE: no", "no", SMTPAuthNoAuth},
{"PLAIN", "plain", SMTPAuthPlain},
{"PLAIN-NOENC", "plain-noenc", SMTPAuthPlainNoEnc},
{"SCRAM-SHA-1: scram-sha-1", "scram-sha-1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1: scram-sha1", "scram-sha1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1: scramsha1", "scramsha1", SMTPAuthSCRAMSHA1},
Expand Down
14 changes: 12 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1096,12 +1096,22 @@
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
case SMTPAuthPlainNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)

Check warning on line 1104 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L1100-L1104

Added lines #L1100 - L1104 were not covered by tests
case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
case SMTPAuthLoginNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)

Check warning on line 1114 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L1110-L1114

Added lines #L1110 - L1114 were not covered by tests
case SMTPAuthCramMD5:
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported
Expand Down
12 changes: 6 additions & 6 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
{
"WithSMTPAuthCustom()",
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")),
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
false,
},
{"WithUsername()", WithUsername("test"), false},
Expand Down Expand Up @@ -605,8 +605,8 @@ func TestSetSMTPAuthCustom(t *testing.T) {
sf bool
}{
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
}
si := smtp.ServerInfo{TLS: true}
for _, tt := range tests {
Expand Down Expand Up @@ -807,7 +807,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
}
c.user = "invalid"
c.pass = "invalid"
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false))
ctx := context.Background()
if err = c.DialWithContext(ctx); err == nil {
t.Errorf("dial succeeded but was supposed to fail")
Expand Down Expand Up @@ -1227,7 +1227,7 @@ func TestClient_DialWithContext_switchAuth(t *testing.T) {

// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST")))
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false))
if client.smtpAuthType != SMTPAuthCustom {
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
}
Expand Down Expand Up @@ -1955,7 +1955,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
wg.Wait()

if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
t.Logf("failed to close server connection: %s", err)
}
}

Expand Down
13 changes: 7 additions & 6 deletions smtp/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (

// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
type loginAuth struct {
username, password string
host string
respStep uint8
username, password string
host string
respStep uint8
allowUnencryptedAuth bool
}

// LoginAuth returns an [Auth] that implements the LOGIN authentication
Expand All @@ -35,8 +36,8 @@ type loginAuth struct {
// LoginAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func LoginAuth(username, password, host string) Auth {
return &loginAuth{username, password, host, 0}
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
return &loginAuth{username, password, host, 0, allowUnEnc}
}

// Start begins the SMTP authentication process by validating server's TLS status and hostname.
Expand All @@ -47,7 +48,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
// In particular, it doesn't matter if the server advertises LOGIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
Expand Down
7 changes: 4 additions & 3 deletions smtp/auth_plain.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package smtp
type plainAuth struct {
identity, username, password string
host string
allowUnencryptedAuth bool
}

// PlainAuth returns an [Auth] that implements the PLAIN authentication
Expand All @@ -27,8 +28,8 @@ type plainAuth struct {
// PlainAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func PlainAuth(identity, username, password, host string) Auth {
return &plainAuth{identity, username, password, host}
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
return &plainAuth{identity, username, password, host, allowUnEnc}
}

func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
Expand All @@ -37,7 +38,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
// In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
Expand Down
4 changes: 2 additions & 2 deletions smtp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var (
func ExamplePlainAuth() {
// hostname is used by PlainAuth to validate the TLS certificate.
hostname := "mail.example.com"
auth := smtp.PlainAuth("", "[email protected]", "password", hostname)
auth := smtp.PlainAuth("", "[email protected]", "password", hostname, false)

err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
if err != nil {
Expand All @@ -77,7 +77,7 @@ func ExamplePlainAuth() {

func ExampleSendMail() {
// Set up authentication information.
auth := smtp.PlainAuth("", "[email protected]", "password", "mail.example.com")
auth := smtp.PlainAuth("", "[email protected]", "password", "mail.example.com", false)

// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
Expand Down
Loading
Loading