Files
zitadel/internal/notification/channels/smtp/xoauth2_auth.go
T
3fc0ef2506 feat: added xoauth for smtp (#11239)
# Which Problems Are Solved

- Some mail providers (e.g.: MS Exchange) have deprecated plain auth

# How the Problems Are Solved

Adds XOauth2 auth option to SMPT
- SMTP config now can contain XOAuth2 auth config
- Proto files are updated to have a oneof for configuration selection.
Old fields for configuring plain auth are still available not to create
a breaking change. These can be removed in the future.
- Columns are added to the database to persist the config

# Additional Changes

none

# Additional Context

Replace this example with links to related issues, discussions, discord
threads, or other sources with more context.
Use the Closing #issue syntax for issues that are resolved with this PR.
- Closes #8042

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
2026-01-27 15:11:56 +01:00

97 lines
2.4 KiB
Go

package smtp
import (
"context"
"hash/fnv"
"net/smtp"
lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"github.com/zitadel/zitadel/internal/zerrors"
)
var tokenSourceCache *lru.TwoQueueCache[string, oauth2.TokenSource]
func init() {
var err error
tokenSourceCache, err = lru.New2Q[string, oauth2.TokenSource](100)
if err != nil {
panic(err)
}
}
type XOAuth2AuthConfig struct {
User string
TokenEndpoint string
Scopes []string
ClientCredentialsAuth *OAuth2ClientCredentials
}
type OAuth2ClientCredentials struct {
ClientId string
ClientSecret string
}
func (cfg XOAuth2AuthConfig) Hash() string {
hash := fnv.New128()
hash.Write([]byte(cfg.User))
hash.Write([]byte(cfg.TokenEndpoint))
for _, s := range cfg.Scopes {
hash.Write([]byte(s))
}
if cfg.ClientCredentialsAuth != nil {
hash.Write([]byte(cfg.ClientCredentialsAuth.ClientId))
hash.Write([]byte(cfg.ClientCredentialsAuth.ClientSecret))
}
return string(hash.Sum(nil))
}
type xoauth2Auth struct {
host string
config XOAuth2AuthConfig
}
func XOAuth2Auth(config XOAuth2AuthConfig, host string) smtp.Auth {
return &xoauth2Auth{
host: host,
config: config,
}
}
func (a *xoauth2Auth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if server.Name != a.host {
return "", nil, zerrors.ThrowInternal(nil, "SMTP-eRJLyi", "wrong host name")
}
hash := a.config.Hash()
tokenSource, ok := tokenSourceCache.Get(hash)
if !ok {
if a.config.ClientCredentialsAuth != nil {
config := &clientcredentials.Config{
ClientID: a.config.ClientCredentialsAuth.ClientId,
ClientSecret: a.config.ClientCredentialsAuth.ClientSecret,
Scopes: a.config.Scopes,
TokenURL: a.config.TokenEndpoint,
}
tokenSource = config.TokenSource(context.Background())
tokenSourceCache.Add(hash, tokenSource)
}
}
token, err := tokenSource.Token()
if err != nil {
return "", nil, zerrors.ThrowInternal(err, "SMTP-fjHcJu", "Failed to get token to connect with smtp server")
}
resp := []byte("user=" + a.config.User + "\x01" + "auth=Bearer " + token.AccessToken + "\x01\x01")
return "XOAUTH2", resp, nil
}
func (a *xoauth2Auth) Next(_ []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
return nil, zerrors.ThrowInternal(nil, "SMTP-Pqsrj9", "unexpected server challenge for XOAUTH2 auth method")
}