summaryrefslogtreecommitdiffstats
path: root/services/auth/source/smtp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /services/auth/source/smtp
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.HEADupstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'services/auth/source/smtp')
-rw-r--r--services/auth/source/smtp/assert_interface_test.go24
-rw-r--r--services/auth/source/smtp/auth.go106
-rw-r--r--services/auth/source/smtp/source.go66
-rw-r--r--services/auth/source/smtp/source_authenticate.go92
4 files changed, 288 insertions, 0 deletions
diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go
new file mode 100644
index 0000000..6c9cde6
--- /dev/null
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -0,0 +1,24 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package smtp_test
+
+import (
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/auth/source/smtp"
+)
+
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
+type sourceInterface interface {
+ auth.PasswordAuthenticator
+ auth_model.Config
+ auth_model.SkipVerifiable
+ auth_model.HasTLSer
+ auth_model.UseTLSer
+ auth_model.SourceSettable
+}
+
+var _ (sourceInterface) = &smtp.Source{}
diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go
new file mode 100644
index 0000000..6446fcd
--- /dev/null
+++ b/services/auth/source/smtp/auth.go
@@ -0,0 +1,106 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package smtp
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net"
+ "net/smtp"
+ "os"
+ "strconv"
+)
+
+// _________ __________________________
+// / _____/ / \__ ___/\______ \
+// \_____ \ / \ / \| | | ___/
+// / \/ Y \ | | |
+// /_______ /\____|__ /____| |____|
+// \/ \/
+
+type loginAuthenticator struct {
+ username, password string
+}
+
+func (auth *loginAuthenticator) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte(auth.username), nil
+}
+
+func (auth *loginAuthenticator) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(auth.username), nil
+ case "Password:":
+ return []byte(auth.password), nil
+ }
+ }
+ return nil, nil
+}
+
+// SMTP authentication type names.
+const (
+ PlainAuthentication = "PLAIN"
+ LoginAuthentication = "LOGIN"
+ CRAMMD5Authentication = "CRAM-MD5"
+)
+
+// Authenticators contains available SMTP authentication type names.
+var Authenticators = []string{PlainAuthentication, LoginAuthentication, CRAMMD5Authentication}
+
+// ErrUnsupportedLoginType login source is unknown error
+var ErrUnsupportedLoginType = errors.New("Login source is unknown")
+
+// Authenticate performs an SMTP authentication.
+func Authenticate(a smtp.Auth, source *Source) error {
+ tlsConfig := &tls.Config{
+ InsecureSkipVerify: source.SkipVerify,
+ ServerName: source.Host,
+ }
+
+ conn, err := net.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)))
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ if source.UseTLS() {
+ conn = tls.Client(conn, tlsConfig)
+ }
+
+ client, err := smtp.NewClient(conn, source.Host)
+ if err != nil {
+ return fmt.Errorf("failed to create NewClient: %w", err)
+ }
+ defer client.Close()
+
+ if !source.DisableHelo {
+ hostname := source.HeloHostname
+ if len(hostname) == 0 {
+ hostname, err = os.Hostname()
+ if err != nil {
+ return fmt.Errorf("failed to find Hostname: %w", err)
+ }
+ }
+
+ if err = client.Hello(hostname); err != nil {
+ return fmt.Errorf("failed to send Helo: %w", err)
+ }
+ }
+
+ // If not using SMTPS, always use STARTTLS if available
+ hasStartTLS, _ := client.Extension("STARTTLS")
+ if !source.UseTLS() && hasStartTLS {
+ if err = client.StartTLS(tlsConfig); err != nil {
+ return fmt.Errorf("failed to start StartTLS: %w", err)
+ }
+ }
+
+ if ok, _ := client.Extension("AUTH"); ok {
+ return client.Auth(a)
+ }
+
+ return ErrUnsupportedLoginType
+}
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
new file mode 100644
index 0000000..2a648e4
--- /dev/null
+++ b/services/auth/source/smtp/source.go
@@ -0,0 +1,66 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package smtp
+
+import (
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/json"
+)
+
+// _________ __________________________
+// / _____/ / \__ ___/\______ \
+// \_____ \ / \ / \| | | ___/
+// / \/ Y \ | | |
+// /_______ /\____|__ /____| |____|
+// \/ \/
+
+// Source holds configuration for the SMTP login source.
+type Source struct {
+ Auth string
+ Host string
+ Port int
+ AllowedDomains string `xorm:"TEXT"`
+ ForceSMTPS bool
+ SkipVerify bool
+ HeloHostname string
+ DisableHelo bool
+ SkipLocalTwoFA bool `json:",omitempty"`
+
+ // reference to the authSource
+ authSource *auth.Source
+}
+
+// FromDB fills up an SMTPConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+ return json.UnmarshalHandleDoubleEncode(bs, &source)
+}
+
+// ToDB exports an SMTPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+ return json.Marshal(source)
+}
+
+// IsSkipVerify returns if SkipVerify is set
+func (source *Source) IsSkipVerify() bool {
+ return source.SkipVerify
+}
+
+// HasTLS returns true for SMTP
+func (source *Source) HasTLS() bool {
+ return true
+}
+
+// UseTLS returns if TLS is set
+func (source *Source) UseTLS() bool {
+ return source.ForceSMTPS || source.Port == 465
+}
+
+// SetAuthSource sets the related AuthSource
+func (source *Source) SetAuthSource(authSource *auth.Source) {
+ source.authSource = authSource
+}
+
+func init() {
+ auth.RegisterTypeConfig(auth.SMTP, &Source{})
+}
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
new file mode 100644
index 0000000..1f0a61c
--- /dev/null
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -0,0 +1,92 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package smtp
+
+import (
+ "context"
+ "errors"
+ "net/smtp"
+ "net/textproto"
+ "strings"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// Authenticate queries if the provided login/password is authenticates against the SMTP server
+// Users will be autoregistered as required
+func (source *Source) Authenticate(ctx context.Context, user *user_model.User, userName, password string) (*user_model.User, error) {
+ // Verify allowed domains.
+ if len(source.AllowedDomains) > 0 {
+ idx := strings.Index(userName, "@")
+ if idx == -1 {
+ return nil, user_model.ErrUserNotExist{Name: userName}
+ } else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), userName[idx+1:], true) {
+ return nil, user_model.ErrUserNotExist{Name: userName}
+ }
+ }
+
+ var auth smtp.Auth
+ switch source.Auth {
+ case PlainAuthentication:
+ auth = smtp.PlainAuth("", userName, password, source.Host)
+ case LoginAuthentication:
+ auth = &loginAuthenticator{userName, password}
+ case CRAMMD5Authentication:
+ auth = smtp.CRAMMD5Auth(userName, password)
+ default:
+ return nil, errors.New("unsupported SMTP auth type")
+ }
+
+ if err := Authenticate(auth, source); err != nil {
+ // Check standard error format first,
+ // then fallback to worse case.
+ tperr, ok := err.(*textproto.Error)
+ if (ok && tperr.Code == 535) ||
+ strings.Contains(err.Error(), "Username and Password not accepted") {
+ return nil, user_model.ErrUserNotExist{Name: userName}
+ }
+ if (ok && tperr.Code == 534) ||
+ strings.Contains(err.Error(), "Application-specific password required") {
+ return nil, user_model.ErrUserNotExist{Name: userName}
+ }
+ return nil, err
+ }
+
+ if user != nil {
+ return user, nil
+ }
+
+ username := userName
+ idx := strings.Index(userName, "@")
+ if idx > -1 {
+ username = userName[:idx]
+ }
+
+ user = &user_model.User{
+ LowerName: strings.ToLower(username),
+ Name: strings.ToLower(username),
+ Email: userName,
+ Passwd: password,
+ LoginType: auth_model.SMTP,
+ LoginSource: source.authSource.ID,
+ LoginName: userName,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: optional.Some(true),
+ }
+
+ if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
+ return user, err
+ }
+
+ return user, nil
+}
+
+// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
+func (source *Source) IsSkipLocalTwoFA() bool {
+ return source.SkipLocalTwoFA
+}