SMTP: Support SASL XOAUTH2
Signed-off-by: Jan-Otto Kröpke <joe@cloudeteer.de>
This commit is contained in:
parent
8e31af479f
commit
c6736a2ae8
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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/" ]
|
Loading…
Reference in New Issue