Implement SMTP StartTLS and Adaptive Auth

- If the STARTTLS extension is advertised we automatically STARTTLS before authenticating or sending
- Uses the secure config key to determine if we should verify the cert. By default it does not verify the cert (should not break any configs)
- Attempt auth when the config has a SMTP password and the server supports the AUTH extension and either the PLAIN or LOGIN mechanism
- Check the mechanisms supported by the server and use PLAIN or LOGIN depending on which is supported
- Changed secure key to use boolean values instead of strings
- Arranged SMTP notifier properties/vars to be in the same order
- Log the steps for STARTTLS (debug only)
- Log the steps for AUTH (debug only)
This commit is contained in:
James Elliott 2019-12-21 05:40:01 +11:00 committed by Clément Michaud
parent 716e017521
commit c4b56a6002
3 changed files with 113 additions and 34 deletions

View File

@ -17,10 +17,10 @@ type EmailNotifierConfiguration struct {
type SMTPNotifierConfiguration struct { type SMTPNotifierConfiguration struct {
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
Secure string `yaml:"secure"` Sender string `yaml:"sender"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Sender string `yaml:"sender"` Secure bool `yaml:"secure"`
} }
// NotifierConfiguration representes the configuration of the notifier to use when sending notifications to users. // NotifierConfiguration representes the configuration of the notifier to use when sending notifications to users.

View File

@ -0,0 +1,34 @@
package notification
import (
"bytes"
"fmt"
"net/smtp"
)
type loginAuth struct {
username string
password string
}
func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch {
case bytes.Equal(fromServer, []byte("Username:")):
return []byte(a.username), nil
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, fmt.Errorf("Unexpected challenge/data from server: %s.", fromServer)
}
}

View File

@ -1,52 +1,112 @@
package notification package notification
import ( import (
"crypto/tls"
"errors"
"fmt" "fmt"
"net/smtp" "net/smtp"
"strings"
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
log "github.com/sirupsen/logrus"
) )
// SMTPNotifier a notifier to send emails to SMTP servers. // SMTPNotifier a notifier to send emails to SMTP servers.
type SMTPNotifier struct { type SMTPNotifier struct {
address string
sender string
username string username string
password string password string
sender string
host string host string
port int port int
secure bool
address string
} }
// NewSMTPNotifier create an SMTPNotifier targeting a given address. // NewSMTPNotifier create an SMTPNotifier targeting a given address.
func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier { func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier {
return &SMTPNotifier{ return &SMTPNotifier{
host: configuration.Host,
port: configuration.Port,
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
sender: configuration.Sender,
username: configuration.Username, username: configuration.Username,
password: configuration.Password, password: configuration.Password,
sender: configuration.Sender,
host: configuration.Host,
port: configuration.Port,
secure: configuration.Secure,
address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port),
} }
} }
func (n *SMTPNotifier) authenticatedSend(recipient string, msg string) error { // Send send a identity verification link to a user.
auth := smtp.PlainAuth("", n.username, n.password, n.host) func (n *SMTPNotifier) Send(recipient string, subject string, body string) error {
err := smtp.SendMail(fmt.Sprintf("%s:%d", n.host, n.port), auth, n.sender, msg := "From: " + n.sender + "\n" +
[]string{recipient}, []byte(msg)) "To: " + recipient + "\n" +
if err != nil { "Subject: " + subject + "\n" +
return err "Content-Type: text/html\n" +
} "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
return nil body
}
func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error {
// Connect to the remote SMTP server.
c, err := smtp.Dial(n.address) c, err := smtp.Dial(n.address)
if err != nil { if err != nil {
return err return err
} }
// Do StartTLS if available (some servers only provide the auth extnesion after, and encrpytion is preferred)
starttls, _ := c.Extension("STARTTLS")
if starttls {
tlsconfig := &tls.Config{
InsecureSkipVerify: !n.secure,
ServerName: n.host,
}
log.Debugf("SMTP server supports STARTTLS (InsecureSkipVerify: %t, ServerName: %s), attempting", tlsconfig.InsecureSkipVerify, tlsconfig.ServerName)
err := c.StartTLS(tlsconfig)
if err != nil {
return err
} else {
log.Debug("SMTP STARTTLS completed without error")
}
} else {
log.Debug("SMTP server does not support STARTTLS, skipping")
}
// Attempt AUTH if password is specified only
if n.password != "" {
// Check the server supports AUTH, and get the mechanisms
authExtension, m := c.Extension("AUTH")
if authExtension {
log.Debugf("Config has SMTP password and server supports AUTH with the following mechanisms: %s.", m)
mechanisms := strings.Split(m, " ")
var auth smtp.Auth
// Adaptively select the AUTH mechanism to use based on what the server advertised
if utils.IsStringInSlice("PLAIN", mechanisms) {
auth = smtp.PlainAuth("", n.username, n.password, n.host)
log.Debug("SMTP server supports AUTH PLAIN, attempting...")
} else if utils.IsStringInSlice("LOGIN", mechanisms) {
auth = LoginAuth(n.username, n.password)
log.Debug("SMTP server supports AUTH LOGIN, attempting...")
}
// Throw error since AUTH extension is not supported
if auth == nil {
return fmt.Errorf("SMTP server does not advertise a AUTH mechanism that Authelia supports. Advertised mechanisms: %s.", m)
}
// Authenticate
err := c.Auth(auth)
if err != nil {
return err
} else {
log.Debug("SMTP AUTH completed successfully.")
}
} else {
return errors.New("SMTP server does not advertise the AUTH extension but a password was specified. Either disable auth (don't specify a password/comment the password), or specify an SMTP host and port that supports AUTH PLAIN or AUTH LOGIN.")
}
} else {
log.Debug("SMTP config has no password specified for use with AUTH, skipping.")
}
// Set the sender and recipient first // Set the sender and recipient first
if err := c.Mail(n.sender); err != nil { if err := c.Mail(n.sender); err != nil {
return err return err
@ -77,18 +137,3 @@ func (n *SMTPNotifier) unauthenticatedSend(recipient string, msg string) error {
} }
return nil return nil
} }
// Send send a identity verification link to a user.
func (n *SMTPNotifier) Send(recipient string, subject string, body string) error {
msg := "From: " + n.sender + "\n" +
"To: " + recipient + "\n" +
"Subject: " + subject + "\n" +
"Content-Type: text/html\n" +
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
body
if n.password != "" {
return n.authenticatedSend(recipient, msg)
}
return n.unauthenticatedSend(recipient, msg)
}