Add support for PLAIN authentication without encryption

Implemented a new SMTPAuthPlainNoEnc option to allow PLAIN authentication over unencrypted connections. Refactored the PlainAuth function to accept an additional allowUnencryptedAuth parameter. Updated relevant tests to cover the new authentication method.
This commit is contained in:
Winni Neessen 2024-10-22 15:30:15 +02:00
parent 91caf200ec
commit f5531eae14
Signed by: wneessen
GPG key ID: 385AC9889632126E
4 changed files with 27 additions and 7 deletions

14
auth.go
View file

@ -62,6 +62,20 @@ const (
// https://datatracker.ietf.org/doc/html/rfc4616/ // https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlain SMTPAuthType = "PLAIN" 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, bye 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. // SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol // https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2" SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"

View file

@ -1096,7 +1096,12 @@ func (c *Client) auth() error {
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) { if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported 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)
case SMTPAuthLogin: case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) { if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported return ErrLoginAuthNotSupported

View file

@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false}, {"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
{ {
"WithSMTPAuthCustom()", "WithSMTPAuthCustom()",
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")), WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
false, false,
}, },
{"WithUsername()", WithUsername("test"), false}, {"WithUsername()", WithUsername("test"), false},
@ -606,7 +606,7 @@ func TestSetSMTPAuthCustom(t *testing.T) {
}{ }{
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false}, {"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false}, {"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false}, {"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
} }
si := smtp.ServerInfo{TLS: true} si := smtp.ServerInfo{TLS: true}
for _, tt := range tests { for _, tt := range tests {
@ -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 // We switch to CUSTOM by providing PLAIN auth as function - the server supports this
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"), 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 { if client.smtpAuthType != SMTPAuthCustom {
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType) t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
} }

View file

@ -17,6 +17,7 @@ package smtp
type plainAuth struct { type plainAuth struct {
identity, username, password string identity, username, password string
host string host string
allowUnencryptedAuth bool
} }
// PlainAuth returns an [Auth] that implements the PLAIN authentication // PlainAuth returns an [Auth] that implements the PLAIN authentication
@ -27,8 +28,8 @@ type plainAuth struct {
// PlainAuth will only send the credentials if the connection is using TLS // PlainAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an // or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials. // error, without sending the credentials.
func PlainAuth(identity, username, password, host string) Auth { func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
return &plainAuth{identity, username, password, host} return &plainAuth{identity, username, password, host, allowUnEnc}
} }
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
@ -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. // In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying // That might just be the attacker saying
// "it's ok, you can trust me with your password." // "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 return "", nil, ErrUnencrypted
} }
if server.Name != a.host { if server.Name != a.host {