SMTP: Support SASL XOAUTH2

Signed-off-by: Jan-Otto Kröpke <joe@cloudeteer.de>
This commit is contained in:
Jan-Otto Kröpke 2024-11-19 12:41:55 +01:00
parent 8e31af479f
commit c6736a2ae8
No known key found for this signature in database
3 changed files with 144 additions and 18 deletions

View File

@ -11,21 +11,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Some tests require a running mail catcher. We use MailDev for this purpose, // Some tests require a working OAUTH2 smtp server.
// it can work without or with authentication (LOGIN only). It exposes a REST // At the time of writing, the only available server are Google's and Microsoft's.
// API which we use to retrieve and check the sent emails. // Follow the instructions on the respective pages to set up the client configuration:
// * https://learn.microsoft.com/de-de/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
// * https://developers.google.com/gmail/imap/xoauth2-protocol
// //
// Those tests are only executed when specific environment variables are set, // To run the tests locally, run:
// otherwise they are skipped. The tests must be run by the CI. // $ EMAIL_AUTH_XOAUTH2_CONFIG=testdata/auth_xoauth2.yml make
//
// To run the tests locally, you should start 2 MailDev containers:
//
// $ docker run --rm -p 1080:1080 -p 1025:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa -v
// $ docker run --rm -p 1081:1080 -p 1026:1025 --entrypoint bin/maildev djfarrelly/maildev@sha256:624e0ec781e11c3531da83d9448f5861f258ee008c1b2da63b3248bfd680acfa --incoming-user user --incoming-pass pass -v
//
// $ EMAIL_NO_AUTH_CONFIG=testdata/noauth.yml EMAIL_AUTH_CONFIG=testdata/auth.yml make
//
// See also https://github.com/djfarrelly/MailDev for more details.
package email package email
import ( import (
@ -36,6 +29,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"strconv" "strconv"
"testing" "testing"
"time" "time"
@ -53,6 +47,8 @@ import (
) )
const ( const (
emailAuthXOAuth2ConfigVar = "EMAIL_AUTH_XOAUTH2_CONFIG"
TestBearerUsername = "fxcp" TestBearerUsername = "fxcp"
TestBearerToken = "VkIvciKi9ijpiKNWrQmYCJrzgd9QYCMB" TestBearerToken = "VkIvciKi9ijpiKNWrQmYCJrzgd9QYCMB"
) )
@ -200,3 +196,125 @@ func (*xOAuth2BackendAuth) Next(response []byte) ([]byte, bool, error) {
return nil, true, fmt.Errorf("unexpected token: %s, expected: %s", token, expectedToken) return nil, true, fmt.Errorf("unexpected token: %s, expected: %s", token, expectedToken)
} }
// TestEmailNotifyWithXOAuth2Authentication sends emails to an instance of MailDev
// configured with authentication.
func TestEmailNotifyWithXOAuth2Authentication(t *testing.T) {
cfgFile := os.Getenv(emailAuthXOAuth2ConfigVar)
if len(cfgFile) == 0 {
t.Skipf("%s not set", emailAuthXOAuth2ConfigVar)
}
c, err := loadEmailTestConfiguration(cfgFile)
if err != nil {
t.Fatal(err)
}
fileWithCorrectClientSecret, err := os.CreateTemp("", "client-secret-correct")
require.NoError(t, err, "creating temp file failed")
_, err = fileWithCorrectClientSecret.WriteString(string(c.XOAuth2.ClientSecret))
require.NoError(t, err, "writing to temp file failed")
fileWithIncorrectClientSecret, err := os.CreateTemp("", "client-secret-incorrect")
require.NoError(t, err, "creating temp file failed")
_, err = fileWithIncorrectClientSecret.WriteString(string(c.XOAuth2.ClientSecret) + "wrong")
require.NoError(t, err, "writing to temp file failed")
for _, tc := range []struct {
title string
updateCfg func(*config.EmailConfig)
errMsg string
retry bool
}{
{
title: "email with authentication",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthXOAuth2 = c.XOAuth2
},
},
{
title: "email with authentication (password from file)",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthXOAuth2 = c.XOAuth2
cfg.AuthXOAuth2.ClientSecret = ""
cfg.AuthXOAuth2.ClientSecretFile = fileWithCorrectClientSecret.Name()
},
},
{
title: "wrong credentials",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthXOAuth2 = c.XOAuth2
cfg.AuthXOAuth2.ClientSecret = cfg.AuthXOAuth2.ClientSecret + "wrong"
},
errMsg: "Invalid username or password",
retry: true,
},
{
title: "wrong credentials (password from file)",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthXOAuth2 = c.XOAuth2
cfg.AuthXOAuth2.ClientSecret = ""
cfg.AuthXOAuth2.ClientSecretFile = fileWithIncorrectClientSecret.Name()
},
errMsg: "Invalid username or password",
retry: true,
},
{
title: "wrong credentials (missing password file)",
updateCfg: func(cfg *config.EmailConfig) {
cfg.AuthUsername = c.Username
cfg.AuthXOAuth2 = c.XOAuth2
cfg.AuthXOAuth2.ClientSecret = ""
cfg.AuthXOAuth2.ClientSecretFile = "/does/not/exist"
},
errMsg: "could not read",
retry: true,
},
{
title: "no credentials",
errMsg: "authentication Required",
retry: true,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
emailCfg := &config.EmailConfig{
TLSConfig: &commoncfg.TLSConfig{},
Smarthost: c.Smarthost,
To: emailTo,
From: emailFrom,
HTML: "HTML body",
Text: "Text body",
Headers: map[string]string{
"Subject": "{{ len .Alerts }} {{ .Status }} alert(s)",
},
}
if c.Smarthost.Port == "587" {
requireTLS := true
emailCfg.RequireTLS = &requireTLS
}
if tc.updateCfg != nil {
tc.updateCfg(emailCfg)
}
tmpl, firingAlert, err := prepare(emailCfg)
require.NoError(t, err)
email := New(emailCfg, tmpl, promslog.NewNopLogger())
retry, err := email.Notify(context.Background(), firingAlert)
require.NoError(t, err)
require.Equal(t, tc.retry, retry)
})
}
}

View File

@ -147,6 +147,7 @@ type emailTestConfig struct {
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
Server *mailDev `yaml:"server"` Server *mailDev `yaml:"server"`
XOAuth2 *commoncfg.OAuth2 `yaml:"xoauth2,omitempty"`
} }
func loadEmailTestConfiguration(f string) (emailTestConfig, error) { func loadEmailTestConfiguration(f string) (emailTestConfig, error) {

View File

@ -0,0 +1,7 @@
smarthost: smtp.gmail.com:587
username: ""
xoauth2:
client_id:
client_secret:
token_url: "https://oauth2.googleapis.com/token"
scopes: [ "https://mail.google.com/" ]