Browse Source

auth: decouple types and functions from db (#6320)

ᴜɴᴋɴᴡᴏɴ 4 years ago
parent
commit
3af91d7cfd

+ 5 - 5
internal/assets/conf/conf_gen.go

@@ -384,7 +384,7 @@ func confAuthDLdap_simple_authConfExample() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "conf/auth.d/ldap_simple_auth.conf.example", size: 761, mode: os.FileMode(0644), modTime: time.Unix(1599160112, 0)}
+	info := bindataFileInfo{name: "conf/auth.d/ldap_simple_auth.conf.example", size: 761, mode: os.FileMode(0644), modTime: time.Unix(1600504559, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x79, 0x97, 0x7b, 0x30, 0x8a, 0x94, 0x93, 0xa7, 0x6e, 0xfc, 0x9e, 0x39, 0xc3, 0xd5, 0x90, 0x25, 0xb8, 0xb9, 0xf2, 0x85, 0xb4, 0x1f, 0xcd, 0x71, 0xf, 0xfa, 0x7b, 0x74, 0x8, 0x5c, 0x53, 0x7f}}
 	return a, nil
 }
@@ -4584,7 +4584,7 @@ func confLocaleLocale_enUsIni() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "conf/locale/locale_en-US.ini", size: 69630, mode: os.FileMode(0644), modTime: time.Unix(1599653855, 0)}
+	info := bindataFileInfo{name: "conf/locale/locale_en-US.ini", size: 69630, mode: os.FileMode(0644), modTime: time.Unix(1600417296, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xea, 0x6c, 0x8a, 0xc4, 0x1, 0x79, 0xc1, 0x39, 0xaf, 0x1, 0xfc, 0x47, 0x8e, 0x3c, 0xce, 0x35, 0x7f, 0xfb, 0x24, 0xd3, 0x26, 0xf0, 0xe3, 0x1a, 0x6d, 0xdf, 0x91, 0x92, 0xd8, 0x2a, 0x93, 0x95}}
 	return a, nil
 }
@@ -4604,7 +4604,7 @@ func confLocaleLocale_esEsIni() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "conf/locale/locale_es-ES.ini", size: 74572, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
+	info := bindataFileInfo{name: "conf/locale/locale_es-ES.ini", size: 74572, mode: os.FileMode(0644), modTime: time.Unix(1600500781, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x10, 0x8e, 0x14, 0x2e, 0xa, 0xdc, 0xf0, 0x4d, 0x11, 0x9e, 0x48, 0xe7, 0x7b, 0x3f, 0x84, 0x2d, 0xab, 0x25, 0x2a, 0x11, 0x4d, 0x20, 0x36, 0x49, 0xa8, 0xe3, 0x75, 0x85, 0x54, 0xac, 0x95, 0xa8}}
 	return a, nil
 }
@@ -4884,7 +4884,7 @@ func confLocaleLocale_ptPtIni() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "conf/locale/locale_pt-PT.ini", size: 73358, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
+	info := bindataFileInfo{name: "conf/locale/locale_pt-PT.ini", size: 73358, mode: os.FileMode(0644), modTime: time.Unix(1600500781, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9e, 0x9, 0x9c, 0xec, 0x51, 0x68, 0x0, 0xab, 0xc1, 0xa2, 0xcf, 0x1b, 0x9, 0xca, 0x7b, 0xbb, 0x3a, 0x38, 0x50, 0x3e, 0x29, 0xc4, 0xbe, 0x84, 0xc, 0x43, 0x35, 0xe8, 0x34, 0x20, 0x31, 0x72}}
 	return a, nil
 }
@@ -5004,7 +5004,7 @@ func confLocaleLocale_ukUaIni() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "conf/locale/locale_uk-UA.ini", size: 100902, mode: os.FileMode(0644), modTime: time.Unix(1600268328, 0)}
+	info := bindataFileInfo{name: "conf/locale/locale_uk-UA.ini", size: 100902, mode: os.FileMode(0644), modTime: time.Unix(1600417296, 0)}
 	a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xb9, 0x96, 0x2f, 0x7f, 0x77, 0x58, 0xac, 0x25, 0x18, 0xbb, 0xe2, 0x9f, 0x19, 0x24, 0x75, 0x81, 0xf0, 0xb2, 0x61, 0x12, 0xad, 0xf6, 0xa9, 0xe7, 0xdc, 0x78, 0x4b, 0x98, 0x5c, 0x58, 0x24, 0xc1}}
 	return a, nil
 }

File diff suppressed because it is too large
+ 1 - 1
internal/assets/public/public_gen.go


File diff suppressed because it is too large
+ 1 - 1
internal/assets/templates/templates_gen.go


+ 69 - 127
internal/auth/auth.go

@@ -5,144 +5,86 @@
 package auth
 
 import (
-	"strings"
+	"fmt"
 
-	"github.com/go-macaron/session"
-	gouuid "github.com/satori/go.uuid"
-	"gopkg.in/macaron.v1"
-	log "unknwon.dev/clog/v2"
+	"gogs.io/gogs/internal/errutil"
+)
 
-	"gogs.io/gogs/internal/conf"
-	"gogs.io/gogs/internal/db"
-	"gogs.io/gogs/internal/tool"
+type Type int
+
+// Note: New type must append to the end of list to maintain backward compatibility.
+const (
+	None   Type = iota
+	Plain       // 1
+	LDAP        // 2
+	SMTP        // 3
+	PAM         // 4
+	DLDAP       // 5
+	GitHub      // 6
 )
 
-func IsAPIPath(url string) bool {
-	return strings.HasPrefix(url, "/api/")
+// Name returns the human-readable name for given authentication type.
+func Name(typ Type) string {
+	return map[Type]string{
+		LDAP:   "LDAP (via BindDN)",
+		DLDAP:  "LDAP (simple auth)", // Via direct bind
+		SMTP:   "SMTP",
+		PAM:    "PAM",
+		GitHub: "GitHub",
+	}[typ]
 }
 
-// SignedInID returns the id of signed in user, along with one bool value which indicates whether user uses token
-// authentication.
-func SignedInID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
-	if !db.HasEngine {
-		return 0, false
-	}
-
-	// Check access token.
-	if IsAPIPath(c.Req.URL.Path) {
-		tokenSHA := c.Query("token")
-		if len(tokenSHA) <= 0 {
-			tokenSHA = c.Query("access_token")
-		}
-		if len(tokenSHA) == 0 {
-			// Well, check with header again.
-			auHead := c.Req.Header.Get("Authorization")
-			if len(auHead) > 0 {
-				auths := strings.Fields(auHead)
-				if len(auths) == 2 && auths[0] == "token" {
-					tokenSHA = auths[1]
-				}
-			}
-		}
-
-		// Let's see if token is valid.
-		if len(tokenSHA) > 0 {
-			t, err := db.AccessTokens.GetBySHA(tokenSHA)
-			if err != nil {
-				if !db.IsErrAccessTokenNotExist(err) {
-					log.Error("GetAccessTokenBySHA: %v", err)
-				}
-				return 0, false
-			}
-			if err = db.AccessTokens.Save(t); err != nil {
-				log.Error("UpdateAccessToken: %v", err)
-			}
-			return t.UserID, true
-		}
-	}
+var _ errutil.NotFound = (*ErrBadCredentials)(nil)
 
-	uid := sess.Get("uid")
-	if uid == nil {
-		return 0, false
-	}
-	if id, ok := uid.(int64); ok {
-		if _, err := db.GetUserByID(id); err != nil {
-			if !db.IsErrUserNotExist(err) {
-				log.Error("Failed to get user by ID: %v", err)
-			}
-			return 0, false
-		}
-		return id, false
-	}
-	return 0, false
+type ErrBadCredentials struct {
+	Args errutil.Args
 }
 
-// SignedInUser returns the user object of signed in user, along with two bool values,
-// which indicate whether user uses HTTP Basic Authentication or token authentication respectively.
-func SignedInUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasicAuth bool, isTokenAuth bool) {
-	if !db.HasEngine {
-		return nil, false, false
-	}
-
-	uid, isTokenAuth := SignedInID(ctx, sess)
-
-	if uid <= 0 {
-		if conf.Auth.EnableReverseProxyAuthentication {
-			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
-			if len(webAuthUser) > 0 {
-				u, err := db.GetUserByName(webAuthUser)
-				if err != nil {
-					if !db.IsErrUserNotExist(err) {
-						log.Error("Failed to get user by name: %v", err)
-						return nil, false, false
-					}
-
-					// Check if enabled auto-registration.
-					if conf.Auth.EnableReverseProxyAutoRegistration {
-						u := &db.User{
-							Name:     webAuthUser,
-							Email:    gouuid.NewV4().String() + "@localhost",
-							Passwd:   webAuthUser,
-							IsActive: true,
-						}
-						if err = db.CreateUser(u); err != nil {
-							// FIXME: should I create a system notice?
-							log.Error("Failed to create user: %v", err)
-							return nil, false, false
-						} else {
-							return u, false, false
-						}
-					}
-				}
-				return u, false, false
-			}
-		}
+func IsErrBadCredentials(err error) bool {
+	_, ok := err.(ErrBadCredentials)
+	return ok
+}
 
-		// Check with basic auth.
-		baHead := ctx.Req.Header.Get("Authorization")
-		if len(baHead) > 0 {
-			auths := strings.Fields(baHead)
-			if len(auths) == 2 && auths[0] == "Basic" {
-				uname, passwd, _ := tool.BasicAuthDecode(auths[1])
+func (err ErrBadCredentials) Error() string {
+	return fmt.Sprintf("bad credentials: %v", err.Args)
+}
 
-				u, err := db.Users.Authenticate(uname, passwd, -1)
-				if err != nil {
-					if !db.IsErrUserNotExist(err) {
-						log.Error("Failed to authenticate user: %v", err)
-					}
-					return nil, false, false
-				}
+func (ErrBadCredentials) NotFound() bool {
+	return true
+}
 
-				return u, true, false
-			}
-		}
-		return nil, false, false
-	}
+// ExternalAccount contains queried information returned by an authenticate provider
+// for an external account.
+type ExternalAccount struct {
+	// REQUIRED: The login to be used for authenticating against the provider.
+	Login string
+	// REQUIRED: The username of the account.
+	Name string
+	// The full name of the account.
+	FullName string
+	// The email address of the account.
+	Email string
+	// The location of the account.
+	Location string
+	// The website of the account.
+	Website string
+	// Whether the user should be prompted as a site admin.
+	Admin bool
+}
 
-	u, err := db.GetUserByID(uid)
-	if err != nil {
-		log.Error("GetUserByID: %v", err)
-		return nil, false, false
-	}
-	return u, false, isTokenAuth
+// Provider defines an authenticate provider which provides ability to authentication against
+// an external identity provider and query external account information.
+type Provider interface {
+	// Authenticate performs authentication against an external identity provider
+	// using given credentials and returns queried information of the external account.
+	Authenticate(login, password string) (*ExternalAccount, error)
+
+	// Config returns the underlying configuration of the authenticate provider.
+	Config() interface{}
+	// HasTLS returns true if the authenticate provider supports TLS.
+	HasTLS() bool
+	// UseTLS returns true if the authenticate provider is configured to use TLS.
+	UseTLS() bool
+	// SkipTLSVerify returns true if the authenticate provider is configured to skip TLS verify.
+	SkipTLSVerify() bool
 }

+ 58 - 0
internal/auth/github/config.go

@@ -0,0 +1,58 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"crypto/tls"
+	"net/http"
+	"strings"
+
+	"github.com/google/go-github/github"
+	"github.com/pkg/errors"
+)
+
+// Config contains configuration for GitHub authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+	// the GitHub service endpoint, e.g. https://api.github.com/.
+	APIEndpoint string
+	SkipVerify  bool
+}
+
+func (c *Config) doAuth(login, password string) (fullname, email, location, website string, err error) {
+	tp := github.BasicAuthTransport{
+		Username: strings.TrimSpace(login),
+		Password: strings.TrimSpace(password),
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipVerify},
+		},
+	}
+	client, err := github.NewEnterpriseClient(c.APIEndpoint, c.APIEndpoint, tp.Client())
+	if err != nil {
+		return "", "", "", "", errors.Wrap(err, "create new client")
+	}
+	user, _, err := client.Users.Get(context.Background(), "")
+	if err != nil {
+		return "", "", "", "", errors.Wrap(err, "get user info")
+	}
+
+	if user.Name != nil {
+		fullname = *user.Name
+	}
+	if user.Email != nil {
+		email = *user.Email
+	} else {
+		email = login + "+github@local"
+	}
+	if user.Location != nil {
+		location = strings.ToUpper(*user.Location)
+	}
+	if user.HTMLURL != nil {
+		website = strings.ToLower(*user.HTMLURL)
+	}
+	return fullname, email, location, website, nil
+}

+ 0 - 50
internal/auth/github/github.go

@@ -1,50 +0,0 @@
-// Copyright 2018 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package github
-
-import (
-	"context"
-	"crypto/tls"
-	"fmt"
-	"net/http"
-	"strings"
-
-	"github.com/google/go-github/github"
-)
-
-func Authenticate(apiEndpoint, login, passwd string) (name string, email string, website string, location string, _ error) {
-	tp := github.BasicAuthTransport{
-		Username: strings.TrimSpace(login),
-		Password: strings.TrimSpace(passwd),
-		Transport: &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-		},
-	}
-	client, err := github.NewEnterpriseClient(apiEndpoint, apiEndpoint, tp.Client())
-	if err != nil {
-		return "", "", "", "", fmt.Errorf("create new client: %v", err)
-	}
-	user, _, err := client.Users.Get(context.Background(), "")
-	if err != nil {
-		return "", "", "", "", fmt.Errorf("get user info: %v", err)
-	}
-
-	if user.Name != nil {
-		name = *user.Name
-	}
-	if user.Email != nil {
-		email = *user.Email
-	} else {
-		email = login + "+github@local"
-	}
-	if user.HTMLURL != nil {
-		website = strings.ToLower(*user.HTMLURL)
-	}
-	if user.Location != nil {
-		location = strings.ToUpper(*user.Location)
-	}
-
-	return name, email, website, location, nil
-}

+ 57 - 0
internal/auth/github/provider.go

@@ -0,0 +1,57 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"strings"
+
+	"gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of a PAM authentication provider.
+type Provider struct {
+	config *Config
+}
+
+// NewProvider creates a new PAM authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+	return &Provider{
+		config: cfg,
+	}
+}
+
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+	fullname, email, website, location, err := p.config.doAuth(login, password)
+	if err != nil {
+		if strings.Contains(err.Error(), "401") {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+		}
+		return nil, err
+	}
+	return &auth.ExternalAccount{
+		Login:    login,
+		Name:     login,
+		FullName: fullname,
+		Email:    email,
+		Location: location,
+		Website:  website,
+	}, nil
+}
+
+func (p *Provider) Config() interface{} {
+	return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+	return true
+}
+
+func (p *Provider) UseTLS() bool {
+	return true
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+	return p.config.SkipVerify
+}

+ 76 - 60
internal/auth/ldap/ldap.go → internal/auth/ldap/config.go

@@ -2,8 +2,8 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-// Package ldap provide functions & structure to query a LDAP ldap directory
-// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
+// Package ldap provide functions & structure to query a LDAP ldap directory.
+// For now, it's mainly tested again an MS Active Directory service, see README.md for more information.
 package ldap
 
 import (
@@ -15,19 +15,31 @@ import (
 	log "unknwon.dev/clog/v2"
 )
 
+// SecurityProtocol is the security protocol when the authenticate provider talks to LDAP directory.
 type SecurityProtocol int
 
-// Note: new type must be added at the end of list to maintain compatibility.
+// ⚠️ WARNING: new type must be added at the end of list to maintain compatibility.
 const (
 	SecurityProtocolUnencrypted SecurityProtocol = iota
 	SecurityProtocolLDAPS
 	SecurityProtocolStartTLS
 )
 
-// Basic LDAP authentication service
-type Source struct {
+// SecurityProtocolName returns the human-readable name for given security protocol.
+func SecurityProtocolName(protocol SecurityProtocol) string {
+	return map[SecurityProtocol]string{
+		SecurityProtocolUnencrypted: "Unencrypted",
+		SecurityProtocolLDAPS:       "LDAPS",
+		SecurityProtocolStartTLS:    "StartTLS",
+	}[protocol]
+}
+
+// Config contains configuration for LDAP authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
 	Host              string // LDAP host
-	Port              int    // port number
+	Port              int    // Port number
 	SecurityProtocol  SecurityProtocol
 	SkipVerify        bool
 	BindDN            string `ini:"bind_dn,omitempty"` // DN to bind with
@@ -37,18 +49,22 @@ type Source struct {
 	AttributeUsername string // Username attribute
 	AttributeName     string // First name attribute
 	AttributeSurname  string // Surname attribute
-	AttributeMail     string // E-mail attribute
-	AttributesInBind  bool   // fetch attributes in bind context (not user)
+	AttributeMail     string // Email attribute
+	AttributesInBind  bool   // Fetch attributes in bind context (not user)
 	Filter            string // Query filter to validate entry
 	AdminFilter       string // Query filter to check if user is admin
-	GroupEnabled      bool   // if the group checking is enabled
-	GroupDN           string `ini:"group_dn"` // Group Search Base
-	GroupFilter       string // Group Name Filter
+	GroupEnabled      bool   // Whether the group checking is enabled
+	GroupDN           string `ini:"group_dn"` // Group search base
+	GroupFilter       string // Group name filter
 	GroupMemberUID    string `ini:"group_member_uid"` // Group Attribute containing array of UserUID
-	UserUID           string `ini:"user_uid"`         // User Attribute listed in Group
+	UserUID           string `ini:"user_uid"`         // User Attribute listed in group
+}
+
+func (c *Config) SecurityProtocolName() string {
+	return SecurityProtocolName(c.SecurityProtocol)
 }
 
-func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
+func (c *Config) sanitizedUserQuery(username string) (string, bool) {
 	// See http://tools.ietf.org/search/rfc4515
 	badCharacters := "\x00()*\\"
 	if strings.ContainsAny(username, badCharacters) {
@@ -56,10 +72,10 @@ func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
 		return "", false
 	}
 
-	return strings.Replace(ls.Filter, "%s", username, -1), true
+	return strings.Replace(c.Filter, "%s", username, -1), true
 }
 
-func (ls *Source) sanitizedUserDN(username string) (string, bool) {
+func (c *Config) sanitizedUserDN(username string) (string, bool) {
 	// See http://tools.ietf.org/search/rfc4514: "special characters"
 	badCharacters := "\x00()*\\,='\"#+;<>"
 	if strings.ContainsAny(username, badCharacters) || strings.HasPrefix(username, " ") || strings.HasSuffix(username, " ") {
@@ -67,10 +83,10 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
 		return "", false
 	}
 
-	return strings.Replace(ls.UserDN, "%s", username, -1), true
+	return strings.Replace(c.UserDN, "%s", username, -1), true
 }
 
-func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
+func (c *Config) sanitizedGroupFilter(group string) (string, bool) {
 	// See http://tools.ietf.org/search/rfc4515
 	badCharacters := "\x00*\\"
 	if strings.ContainsAny(group, badCharacters) {
@@ -81,7 +97,7 @@ func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
 	return group, true
 }
 
-func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
+func (c *Config) sanitizedGroupDN(groupDn string) (string, bool) {
 	// See http://tools.ietf.org/search/rfc4514: "special characters"
 	badCharacters := "\x00()*\\'\"#+;<>"
 	if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
@@ -92,12 +108,12 @@ func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
 	return groupDn, true
 }
 
-func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
+func (c *Config) findUserDN(l *ldap.Conn, name string) (string, bool) {
 	log.Trace("Search for LDAP user: %s", name)
-	if len(ls.BindDN) > 0 && len(ls.BindPassword) > 0 {
+	if len(c.BindDN) > 0 && len(c.BindPassword) > 0 {
 		// Replace placeholders with username
-		bindDN := strings.Replace(ls.BindDN, "%s", name, -1)
-		err := l.Bind(bindDN, ls.BindPassword)
+		bindDN := strings.Replace(c.BindDN, "%s", name, -1)
+		err := l.Bind(bindDN, c.BindPassword)
 		if err != nil {
 			log.Trace("LDAP: Failed to bind as BindDN '%s': %v", bindDN, err)
 			return "", false
@@ -108,23 +124,23 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
 	}
 
 	// A search for the user.
-	userFilter, ok := ls.sanitizedUserQuery(name)
+	userFilter, ok := c.sanitizedUserQuery(name)
 	if !ok {
 		return "", false
 	}
 
-	log.Trace("LDAP: Searching for DN using filter '%s' and base '%s'", userFilter, ls.UserBase)
+	log.Trace("LDAP: Searching for DN using filter %q and base %q", userFilter, c.UserBase)
 	search := ldap.NewSearchRequest(
-		ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
+		c.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
 		false, userFilter, []string{}, nil)
 
 	// Ensure we found a user
 	sr, err := l.Search(search)
 	if err != nil || len(sr.Entries) < 1 {
-		log.Trace("LDAP: Failed search using filter '%s': %v", userFilter, err)
+		log.Trace("LDAP: Failed to search using filter %q: %v", userFilter, err)
 		return "", false
 	} else if len(sr.Entries) > 1 {
-		log.Trace("LDAP: Filter '%s' returned more than one user", userFilter)
+		log.Trace("LDAP: Filter %q returned more than one user", userFilter)
 		return "", false
 	}
 
@@ -137,7 +153,7 @@ func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
 	return userDN, true
 }
 
-func dial(ls *Source) (*ldap.Conn, error) {
+func dial(ls *Config) (*ldap.Conn, error) {
 	log.Trace("LDAP: Dialing with security protocol '%v' without verifying: %v", ls.SecurityProtocol, ls.SkipVerify)
 
 	tlsCfg := &tls.Config{
@@ -174,26 +190,26 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
 	return err
 }
 
-// searchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
-func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
+// searchEntry searches an LDAP source if an entry (name, passwd) is valid and in the specific filter.
+func (c *Config) searchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
 	// See https://tools.ietf.org/search/rfc4513#section-5.1.2
 	if len(passwd) == 0 {
 		log.Trace("authentication failed for '%s' with empty password", name)
 		return "", "", "", "", false, false
 	}
-	l, err := dial(ls)
+	l, err := dial(c)
 	if err != nil {
-		log.Error("LDAP connect failed for '%s': %v", ls.Host, err)
+		log.Error("LDAP connect failed for '%s': %v", c.Host, err)
 		return "", "", "", "", false, false
 	}
 	defer l.Close()
 
 	var userDN string
 	if directBind {
-		log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
+		log.Trace("LDAP will bind directly via UserDN template: %s", c.UserDN)
 
 		var ok bool
-		userDN, ok = ls.sanitizedUserDN(name)
+		userDN, ok = c.sanitizedUserDN(name)
 		if !ok {
 			return "", "", "", "", false, false
 		}
@@ -201,13 +217,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		log.Trace("LDAP will use BindDN")
 
 		var found bool
-		userDN, found = ls.findUserDN(l, name)
+		userDN, found = c.findUserDN(l, name)
 		if !found {
 			return "", "", "", "", false, false
 		}
 	}
 
-	if directBind || !ls.AttributesInBind {
+	if directBind || !c.AttributesInBind {
 		// binds user (checking password) before looking-up attributes in user context
 		err = bindUser(l, userDN, passwd)
 		if err != nil {
@@ -215,18 +231,18 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 	}
 
-	userFilter, ok := ls.sanitizedUserQuery(name)
+	userFilter, ok := c.sanitizedUserQuery(name)
 	if !ok {
 		return "", "", "", "", false, false
 	}
 
-	log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'",
-		ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID, userFilter, userDN)
+	log.Trace("Fetching attributes %q, %q, %q, %q, %q with user filter %q and user DN %q",
+		c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID, userFilter, userDN)
+
 	search := ldap.NewSearchRequest(
 		userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
-		[]string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID},
+		[]string{c.AttributeUsername, c.AttributeName, c.AttributeSurname, c.AttributeMail, c.UserUID},
 		nil)
-
 	sr, err := l.Search(search)
 	if err != nil {
 		log.Error("LDAP: User search failed: %v", err)
@@ -241,27 +257,27 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		return "", "", "", "", false, false
 	}
 
-	username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
-	firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
-	surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
-	mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
-	uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
+	username := sr.Entries[0].GetAttributeValue(c.AttributeUsername)
+	firstname := sr.Entries[0].GetAttributeValue(c.AttributeName)
+	surname := sr.Entries[0].GetAttributeValue(c.AttributeSurname)
+	mail := sr.Entries[0].GetAttributeValue(c.AttributeMail)
+	uid := sr.Entries[0].GetAttributeValue(c.UserUID)
 
 	// Check group membership
-	if ls.GroupEnabled {
-		groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
+	if c.GroupEnabled {
+		groupFilter, ok := c.sanitizedGroupFilter(c.GroupFilter)
 		if !ok {
 			return "", "", "", "", false, false
 		}
-		groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
+		groupDN, ok := c.sanitizedGroupDN(c.GroupDN)
 		if !ok {
 			return "", "", "", "", false, false
 		}
 
-		log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
+		log.Trace("LDAP: Fetching groups '%v' with filter '%s' and base '%s'", c.GroupMemberUID, groupFilter, groupDN)
 		groupSearch := ldap.NewSearchRequest(
 			groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
-			[]string{ls.GroupMemberUID},
+			[]string{c.GroupMemberUID},
 			nil)
 
 		srg, err := l.Search(groupSearch)
@@ -274,9 +290,9 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 
 		isMember := false
-		if ls.UserUID == "dn" {
+		if c.UserUID == "dn" {
 			for _, group := range srg.Entries {
-				for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
+				for _, member := range group.GetAttributeValues(c.GroupMemberUID) {
 					if member == sr.Entries[0].DN {
 						isMember = true
 					}
@@ -284,7 +300,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 			}
 		} else {
 			for _, group := range srg.Entries {
-				for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
+				for _, member := range group.GetAttributeValues(c.GroupMemberUID) {
 					if member == uid {
 						isMember = true
 					}
@@ -293,17 +309,17 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 
 		if !isMember {
-			log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, ls.GroupMemberUID, uid)
+			log.Trace("LDAP: Group membership test failed [username: %s, group_member_uid: %s, user_uid: %s", username, c.GroupMemberUID, uid)
 			return "", "", "", "", false, false
 		}
 	}
 
 	isAdmin := false
-	if len(ls.AdminFilter) > 0 {
-		log.Trace("Checking admin with filter '%s' and base '%s'", ls.AdminFilter, userDN)
+	if len(c.AdminFilter) > 0 {
+		log.Trace("Checking admin with filter '%s' and base '%s'", c.AdminFilter, userDN)
 		search = ldap.NewSearchRequest(
-			userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
-			[]string{ls.AttributeName},
+			userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, c.AdminFilter,
+			[]string{c.AttributeName},
 			nil)
 
 		sr, err = l.Search(search)
@@ -316,7 +332,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
 		}
 	}
 
-	if !directBind && ls.AttributesInBind {
+	if !directBind && c.AttributesInBind {
 		// binds user (checking password) after looking-up attributes in BindDN context
 		err = bindUser(l, userDN, passwd)
 		if err != nil {

+ 78 - 0
internal/auth/ldap/provider.go

@@ -0,0 +1,78 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package ldap
+
+import (
+	"fmt"
+
+	"gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of an LDAP authentication provider.
+type Provider struct {
+	directBind bool
+	config     *Config
+}
+
+// NewProvider creates a new LDAP authentication provider.
+func NewProvider(directBind bool, cfg *Config) auth.Provider {
+	return &Provider{
+		directBind: directBind,
+		config:     cfg,
+	}
+}
+
+// Authenticate queries if login/password is valid against the LDAP directory pool,
+// and returns queried information when succeeded.
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+	username, fn, sn, email, isAdmin, succeed := p.config.searchEntry(login, password, p.directBind)
+	if !succeed {
+		return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+	}
+
+	if len(username) == 0 {
+		username = login
+	}
+	if len(email) == 0 {
+		email = fmt.Sprintf("%s@localhost", username)
+	}
+
+	composeFullName := func(firstname, surname, username string) string {
+		switch {
+		case firstname == "" && surname == "":
+			return username
+		case firstname == "":
+			return surname
+		case surname == "":
+			return firstname
+		default:
+			return firstname + " " + surname
+		}
+	}
+
+	return &auth.ExternalAccount{
+		Login:    login,
+		Name:     username,
+		FullName: composeFullName(fn, sn, username),
+		Email:    email,
+		Admin:    isAdmin,
+	}, nil
+}
+
+func (p *Provider) Config() interface{} {
+	return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+	return p.config.SecurityProtocol > SecurityProtocolUnencrypted
+}
+
+func (p *Provider) UseTLS() bool {
+	return p.config.SecurityProtocol > SecurityProtocolUnencrypted
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+	return p.config.SkipVerify
+}

+ 13 - 0
internal/auth/pam/config.go

@@ -0,0 +1,13 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package pam
+
+// Config contains configuration for PAM authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+	// The name of the PAM service, e.g. system-auth.
+	ServiceName string
+}

+ 6 - 12
internal/auth/pam/pam.go

@@ -7,29 +7,23 @@
 package pam
 
 import (
-	"errors"
-
 	"github.com/msteinert/pam"
+	"github.com/pkg/errors"
 )
 
-func PAMAuth(serviceName, userName, passwd string) error {
-	t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) {
+func (c *Config) doAuth(login, password string) error {
+	t, err := pam.StartFunc(c.ServiceName, login, func(s pam.Style, msg string) (string, error) {
 		switch s {
 		case pam.PromptEchoOff:
-			return passwd, nil
+			return password, nil
 		case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo:
 			return "", nil
 		}
-		return "", errors.New("Unrecognized PAM message style")
+		return "", errors.Errorf("unrecognized PAM message style: %v - %s", s, msg)
 	})
-
 	if err != nil {
 		return err
 	}
 
-	if err = t.Authenticate(0); err != nil {
-		return err
-	}
-
-	return nil
+	return t.Authenticate(0)
 }

+ 2 - 2
internal/auth/pam/pam_stub.go

@@ -7,9 +7,9 @@
 package pam
 
 import (
-	"errors"
+	"github.com/pkg/errors"
 )
 
-func PAMAuth(serviceName, userName, passwd string) error {
+func (c *Config) doAuth(login, password string) error {
 	return errors.New("PAM not supported")
 }

+ 54 - 0
internal/auth/pam/provider.go

@@ -0,0 +1,54 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package pam
+
+import (
+	"strings"
+
+	"gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of a PAM authentication provider.
+type Provider struct {
+	config *Config
+}
+
+// NewProvider creates a new PAM authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+	return &Provider{
+		config: cfg,
+	}
+}
+
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+	err := p.config.doAuth(login, password)
+	if err != nil {
+		if strings.Contains(err.Error(), "Authentication failure") {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+		}
+		return nil, err
+	}
+
+	return &auth.ExternalAccount{
+		Login: login,
+		Name:  login,
+	}, nil
+}
+
+func (p *Provider) Config() interface{} {
+	return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+	return false
+}
+
+func (p *Provider) UseTLS() bool {
+	return false
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+	return false
+}

+ 58 - 0
internal/auth/smtp/config.go

@@ -0,0 +1,58 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package smtp
+
+import (
+	"crypto/tls"
+	"fmt"
+	"net/smtp"
+
+	"github.com/pkg/errors"
+)
+
+// Config contains configuration for SMTP authentication.
+//
+// ⚠️ WARNING: Change to the field name must preserve the INI key name for backward compatibility.
+type Config struct {
+	Auth           string
+	Host           string
+	Port           int
+	AllowedDomains string
+	TLS            bool `ini:"tls"`
+	SkipVerify     bool
+}
+
+func (c *Config) doAuth(auth smtp.Auth) error {
+	client, err := smtp.Dial(fmt.Sprintf("%s:%d", c.Host, c.Port))
+	if err != nil {
+		return err
+	}
+	defer client.Close()
+
+	if err = client.Hello("gogs"); err != nil {
+		return err
+	}
+
+	if c.TLS {
+		if ok, _ := client.Extension("STARTTLS"); ok {
+			if err = client.StartTLS(&tls.Config{
+				InsecureSkipVerify: c.SkipVerify,
+				ServerName:         c.Host,
+			}); err != nil {
+				return err
+			}
+		} else {
+			return errors.New("SMTP server does not support TLS")
+		}
+	}
+
+	if ok, _ := client.Extension("AUTH"); ok {
+		if err = client.Auth(auth); err != nil {
+			return err
+		}
+		return nil
+	}
+	return errors.New("unsupported SMTP authentication method")
+}

+ 132 - 0
internal/auth/smtp/provider.go

@@ -0,0 +1,132 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package smtp
+
+import (
+	"net/smtp"
+	"net/textproto"
+	"strings"
+
+	"github.com/pkg/errors"
+	log "unknwon.dev/clog/v2"
+
+	"gogs.io/gogs/internal/auth"
+)
+
+// Provider contains configuration of an SMTP authentication provider.
+type Provider struct {
+	config *Config
+}
+
+// NewProvider creates a new SMTP authentication provider.
+func NewProvider(cfg *Config) auth.Provider {
+	return &Provider{
+		config: cfg,
+	}
+}
+
+// Authenticate queries if login/password is valid against the SMTP server,
+// and returns queried information when succeeded.
+func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
+	// Verify allowed domains
+	if p.config.AllowedDomains != "" {
+		fields := strings.SplitN(login, "@", 3)
+		if len(fields) != 2 {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+		}
+		domain := fields[1]
+
+		isAllowed := false
+		for _, allowed := range strings.Split(p.config.AllowedDomains, ",") {
+			if domain == allowed {
+				isAllowed = true
+				break
+			}
+		}
+
+		if !isAllowed {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+		}
+	}
+
+	var smtpAuth smtp.Auth
+	switch p.config.Auth {
+	case Plain:
+		smtpAuth = smtp.PlainAuth("", login, password, p.config.Host)
+	case Login:
+		smtpAuth = &smtpLoginAuth{login, password}
+	default:
+		return nil, errors.Errorf("unsupported SMTP authentication type %q", p.config.Auth)
+	}
+
+	if err := p.config.doAuth(smtpAuth); err != nil {
+		log.Trace("SMTP: Authentication failed: %v", err)
+
+		// Check standard error format first, then fallback to the worse case.
+		tperr, ok := err.(*textproto.Error)
+		if (ok && tperr.Code == 535) ||
+			strings.Contains(err.Error(), "Username and Password not accepted") {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
+		}
+		return nil, err
+	}
+
+	username := login
+
+	// NOTE: It is not required to have "@" in `login` for a successful SMTP authentication.
+	idx := strings.Index(login, "@")
+	if idx > -1 {
+		username = login[:idx]
+	}
+
+	return &auth.ExternalAccount{
+		Login: login,
+		Name:  username,
+		Email: login,
+	}, nil
+}
+
+func (p *Provider) Config() interface{} {
+	return p.config
+}
+
+func (p *Provider) HasTLS() bool {
+	return true
+}
+
+func (p *Provider) UseTLS() bool {
+	return p.config.TLS
+}
+
+func (p *Provider) SkipTLSVerify() bool {
+	return p.config.SkipVerify
+}
+
+const (
+	Plain = "PLAIN"
+	Login = "LOGIN"
+)
+
+var AuthTypes = []string{Plain, Login}
+
+type smtpLoginAuth struct {
+	username, password string
+}
+
+func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+	return "LOGIN", []byte(auth.username), nil
+}
+
+func (auth *smtpLoginAuth) 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
+}

+ 139 - 3
internal/context/auth.go

@@ -7,12 +7,18 @@ package context
 import (
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/go-macaron/csrf"
+	"github.com/go-macaron/session"
+	gouuid "github.com/satori/go.uuid"
 	"gopkg.in/macaron.v1"
+	log "unknwon.dev/clog/v2"
 
 	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/tool"
 )
 
 type ToggleOptions struct {
@@ -49,7 +55,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
 			return
 		}
 
-		if !options.SignOutRequired && !options.DisableCSRF && c.Req.Method == "POST" && !auth.IsAPIPath(c.Req.URL.Path) {
+		if !options.SignOutRequired && !options.DisableCSRF && c.Req.Method == "POST" && !isAPIPath(c.Req.URL.Path) {
 			csrf.Validate(c.Context, c.csrf)
 			if c.Written() {
 				return
@@ -59,7 +65,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
 		if options.SignInRequired {
 			if !c.IsLogged {
 				// Restrict API calls with error message.
-				if auth.IsAPIPath(c.Req.URL.Path) {
+				if isAPIPath(c.Req.URL.Path) {
 					c.JSON(http.StatusForbidden, map[string]string{
 						"message": "Only authenticated user is allowed to call APIs.",
 					})
@@ -77,7 +83,7 @@ func Toggle(options *ToggleOptions) macaron.Handler {
 		}
 
 		// Redirect to log in page if auto-signin info is provided and has not signed in.
-		if !options.SignOutRequired && !c.IsLogged && !auth.IsAPIPath(c.Req.URL.Path) &&
+		if !options.SignOutRequired && !c.IsLogged && !isAPIPath(c.Req.URL.Path) &&
 			len(c.GetCookie(conf.Security.CookieUsername)) > 0 {
 			c.SetCookie("redirect_to", url.QueryEscape(conf.Server.Subpath+c.Req.RequestURI), 0, conf.Server.Subpath)
 			c.RedirectSubpath("/user/login")
@@ -93,3 +99,133 @@ func Toggle(options *ToggleOptions) macaron.Handler {
 		}
 	}
 }
+
+func isAPIPath(url string) bool {
+	return strings.HasPrefix(url, "/api/")
+}
+
+// authenticatedUserID returns the ID of the authenticated user, along with a bool value
+// which indicates whether the user uses token authentication.
+func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
+	if !db.HasEngine {
+		return 0, false
+	}
+
+	// Check access token.
+	if isAPIPath(c.Req.URL.Path) {
+		tokenSHA := c.Query("token")
+		if len(tokenSHA) <= 0 {
+			tokenSHA = c.Query("access_token")
+		}
+		if len(tokenSHA) == 0 {
+			// Well, check with header again.
+			auHead := c.Req.Header.Get("Authorization")
+			if len(auHead) > 0 {
+				auths := strings.Fields(auHead)
+				if len(auths) == 2 && auths[0] == "token" {
+					tokenSHA = auths[1]
+				}
+			}
+		}
+
+		// Let's see if token is valid.
+		if len(tokenSHA) > 0 {
+			t, err := db.AccessTokens.GetBySHA(tokenSHA)
+			if err != nil {
+				if !db.IsErrAccessTokenNotExist(err) {
+					log.Error("GetAccessTokenBySHA: %v", err)
+				}
+				return 0, false
+			}
+			if err = db.AccessTokens.Save(t); err != nil {
+				log.Error("UpdateAccessToken: %v", err)
+			}
+			return t.UserID, true
+		}
+	}
+
+	uid := sess.Get("uid")
+	if uid == nil {
+		return 0, false
+	}
+	if id, ok := uid.(int64); ok {
+		if _, err := db.GetUserByID(id); err != nil {
+			if !db.IsErrUserNotExist(err) {
+				log.Error("Failed to get user by ID: %v", err)
+			}
+			return 0, false
+		}
+		return id, false
+	}
+	return 0, false
+}
+
+// authenticatedUser returns the user object of the authenticated user, along with two bool values
+// which indicate whether the user uses HTTP Basic Authentication or token authentication respectively.
+func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *db.User, isBasicAuth bool, isTokenAuth bool) {
+	if !db.HasEngine {
+		return nil, false, false
+	}
+
+	uid, isTokenAuth := authenticatedUserID(ctx, sess)
+
+	if uid <= 0 {
+		if conf.Auth.EnableReverseProxyAuthentication {
+			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
+			if len(webAuthUser) > 0 {
+				u, err := db.GetUserByName(webAuthUser)
+				if err != nil {
+					if !db.IsErrUserNotExist(err) {
+						log.Error("Failed to get user by name: %v", err)
+						return nil, false, false
+					}
+
+					// Check if enabled auto-registration.
+					if conf.Auth.EnableReverseProxyAutoRegistration {
+						u := &db.User{
+							Name:     webAuthUser,
+							Email:    gouuid.NewV4().String() + "@localhost",
+							Passwd:   webAuthUser,
+							IsActive: true,
+						}
+						if err = db.CreateUser(u); err != nil {
+							// FIXME: should I create a system notice?
+							log.Error("Failed to create user: %v", err)
+							return nil, false, false
+						} else {
+							return u, false, false
+						}
+					}
+				}
+				return u, false, false
+			}
+		}
+
+		// Check with basic auth.
+		baHead := ctx.Req.Header.Get("Authorization")
+		if len(baHead) > 0 {
+			auths := strings.Fields(baHead)
+			if len(auths) == 2 && auths[0] == "Basic" {
+				uname, passwd, _ := tool.BasicAuthDecode(auths[1])
+
+				u, err := db.Users.Authenticate(uname, passwd, -1)
+				if err != nil {
+					if !auth.IsErrBadCredentials(err) {
+						log.Error("Failed to authenticate user: %v", err)
+					}
+					return nil, false, false
+				}
+
+				return u, true, false
+			}
+		}
+		return nil, false, false
+	}
+
+	u, err := db.GetUserByID(uid)
+	if err != nil {
+		log.Error("GetUserByID: %v", err)
+		return nil, false, false
+	}
+	return u, false, isTokenAuth
+}

+ 1 - 2
internal/context/context.go

@@ -18,7 +18,6 @@ import (
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
-	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/errutil"
@@ -255,7 +254,7 @@ func Contexter() macaron.Handler {
 		}
 
 		// Get user from session or header when possible
-		c.User, c.IsBasicAuth, c.IsTokenAuth = auth.SignedInUser(c.Context, c.Session)
+		c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(c.Context, c.Session)
 
 		if c.User != nil {
 			c.IsLogged = true

+ 9 - 6
internal/db/backup_test.go

@@ -14,6 +14,9 @@ import (
 	"github.com/pkg/errors"
 	"gorm.io/gorm"
 
+	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/auth/github"
+	"gogs.io/gogs/internal/auth/pam"
 	"gogs.io/gogs/internal/cryptoutil"
 	"gogs.io/gogs/internal/lfsutil"
 	"gogs.io/gogs/internal/testutil"
@@ -79,22 +82,22 @@ func setupDBToDump(t *testing.T, db *gorm.DB) {
 		},
 
 		&LoginSource{
-			Type:      LoginPAM,
+			Type:      auth.PAM,
 			Name:      "My PAM",
 			IsActived: true,
-			Config: &PAMConfig{
+			Provider: pam.NewProvider(&pam.Config{
 				ServiceName: "PAM service",
-			},
+			}),
 			CreatedUnix: 1588568886,
 			UpdatedUnix: 1588572486, // 1 hour later
 		},
 		&LoginSource{
-			Type:      LoginGitHub,
+			Type:      auth.GitHub,
 			Name:      "GitHub.com",
 			IsActived: true,
-			Config: &GitHubConfig{
+			Provider: github.NewProvider(&github.Config{
 				APIEndpoint: "https://api.github.com",
-			},
+			}),
 			CreatedUnix: 1588568886,
 		},
 	}

+ 0 - 33
internal/db/errors/login_source.go

@@ -1,33 +0,0 @@
-// Copyright 2017 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package errors
-
-import "fmt"
-
-type LoginSourceNotActivated struct {
-	SourceID int64
-}
-
-func IsLoginSourceNotActivated(err error) bool {
-	_, ok := err.(LoginSourceNotActivated)
-	return ok
-}
-
-func (err LoginSourceNotActivated) Error() string {
-	return fmt.Sprintf("login source is not activated [source_id: %d]", err.SourceID)
-}
-
-type InvalidLoginSourceType struct {
-	Type interface{}
-}
-
-func IsInvalidLoginSourceType(err error) bool {
-	_, ok := err.(InvalidLoginSourceType)
-	return ok
-}
-
-func (err InvalidLoginSourceType) Error() string {
-	return fmt.Sprintf("invalid login source type [type: %v]", err.Type)
-}

+ 0 - 342
internal/db/login_source.go

@@ -1,342 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-// FIXME: Put this file into its own package and separate into different files based on login sources.
-package db
-
-import (
-	"crypto/tls"
-	"fmt"
-	"net/smtp"
-	"net/textproto"
-	"strings"
-
-	"github.com/go-macaron/binding"
-	"github.com/unknwon/com"
-
-	"gogs.io/gogs/internal/auth/github"
-	"gogs.io/gogs/internal/auth/ldap"
-	"gogs.io/gogs/internal/auth/pam"
-	"gogs.io/gogs/internal/db/errors"
-)
-
-type LoginType int
-
-// Note: new type must append to the end of list to maintain compatibility.
-// TODO: Move to authutil.
-const (
-	LoginNotype LoginType = iota
-	LoginPlain            // 1
-	LoginLDAP             // 2
-	LoginSMTP             // 3
-	LoginPAM              // 4
-	LoginDLDAP            // 5
-	LoginGitHub           // 6
-)
-
-var LoginNames = map[LoginType]string{
-	LoginLDAP:   "LDAP (via BindDN)",
-	LoginDLDAP:  "LDAP (simple auth)", // Via direct bind
-	LoginSMTP:   "SMTP",
-	LoginPAM:    "PAM",
-	LoginGitHub: "GitHub",
-}
-
-// ***********************
-// ----- LDAP config -----
-// ***********************
-
-type LDAPConfig struct {
-	ldap.Source `ini:"config"`
-}
-
-var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
-	ldap.SecurityProtocolUnencrypted: "Unencrypted",
-	ldap.SecurityProtocolLDAPS:       "LDAPS",
-	ldap.SecurityProtocolStartTLS:    "StartTLS",
-}
-
-func (cfg *LDAPConfig) SecurityProtocolName() string {
-	return SecurityProtocolNames[cfg.SecurityProtocol]
-}
-
-func composeFullName(firstname, surname, username string) string {
-	switch {
-	case len(firstname) == 0 && len(surname) == 0:
-		return username
-	case len(firstname) == 0:
-		return surname
-	case len(surname) == 0:
-		return firstname
-	default:
-		return firstname + " " + surname
-	}
-}
-
-// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
-// and create a local user if success when enabled.
-func LoginViaLDAP(login, password string, source *LoginSource, autoRegister bool) (*User, error) {
-	username, fn, sn, mail, isAdmin, succeed := source.Config.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
-	if !succeed {
-		// User not in LDAP, do nothing
-		return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-	}
-
-	if !autoRegister {
-		return nil, nil
-	}
-
-	// Fallback.
-	if len(username) == 0 {
-		username = login
-	}
-	// Validate username make sure it satisfies requirement.
-	if binding.AlphaDashDotPattern.MatchString(username) {
-		return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username)
-	}
-
-	if len(mail) == 0 {
-		mail = fmt.Sprintf("%s@localhost", username)
-	}
-
-	user := &User{
-		LowerName:   strings.ToLower(username),
-		Name:        username,
-		FullName:    composeFullName(fn, sn, username),
-		Email:       mail,
-		LoginSource: source.ID,
-		LoginName:   login,
-		IsActive:    true,
-		IsAdmin:     isAdmin,
-	}
-
-	ok, err := IsUserExist(0, user.Name)
-	if err != nil {
-		return user, err
-	}
-
-	if ok {
-		return user, UpdateUser(user)
-	}
-
-	return user, CreateUser(user)
-}
-
-// ***********************
-// ----- SMTP config -----
-// ***********************
-
-type SMTPConfig struct {
-	Auth           string
-	Host           string
-	Port           int
-	AllowedDomains string
-	TLS            bool `ini:"tls"`
-	SkipVerify     bool
-}
-
-type smtpLoginAuth struct {
-	username, password string
-}
-
-func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
-	return "LOGIN", []byte(auth.username), nil
-}
-
-func (auth *smtpLoginAuth) 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
-}
-
-const (
-	SMTPPlain = "PLAIN"
-	SMTPLogin = "LOGIN"
-)
-
-var SMTPAuths = []string{SMTPPlain, SMTPLogin}
-
-func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
-	c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
-	if err != nil {
-		return err
-	}
-	defer c.Close()
-
-	if err = c.Hello("gogs"); err != nil {
-		return err
-	}
-
-	if cfg.TLS {
-		if ok, _ := c.Extension("STARTTLS"); ok {
-			if err = c.StartTLS(&tls.Config{
-				InsecureSkipVerify: cfg.SkipVerify,
-				ServerName:         cfg.Host,
-			}); err != nil {
-				return err
-			}
-		} else {
-			return errors.New("SMTP server unsupports TLS")
-		}
-	}
-
-	if ok, _ := c.Extension("AUTH"); ok {
-		if err = c.Auth(a); err != nil {
-			return err
-		}
-		return nil
-	}
-	return errors.New("Unsupported SMTP authentication method")
-}
-
-// LoginViaSMTP queries if login/password is valid against the SMTP,
-// and create a local user if success when enabled.
-func LoginViaSMTP(login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
-	// Verify allowed domains.
-	if len(cfg.AllowedDomains) > 0 {
-		idx := strings.Index(login, "@")
-		if idx == -1 {
-			return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-		} else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) {
-			return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-		}
-	}
-
-	var auth smtp.Auth
-	if cfg.Auth == SMTPPlain {
-		auth = smtp.PlainAuth("", login, password, cfg.Host)
-	} else if cfg.Auth == SMTPLogin {
-		auth = &smtpLoginAuth{login, password}
-	} else {
-		return nil, errors.New("Unsupported SMTP authentication type")
-	}
-
-	if err := SMTPAuth(auth, cfg); 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, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-		}
-		return nil, err
-	}
-
-	if !autoRegister {
-		return nil, nil
-	}
-
-	username := login
-	idx := strings.Index(login, "@")
-	if idx > -1 {
-		username = login[:idx]
-	}
-
-	user := &User{
-		LowerName:   strings.ToLower(username),
-		Name:        strings.ToLower(username),
-		Email:       login,
-		Passwd:      password,
-		LoginSource: sourceID,
-		LoginName:   login,
-		IsActive:    true,
-	}
-	return user, CreateUser(user)
-}
-
-// **********************
-// ----- PAM config -----
-// **********************
-
-type PAMConfig struct {
-	// The name of the PAM service, e.g. system-auth.
-	ServiceName string
-}
-
-// LoginViaPAM queries if login/password is valid against the PAM,
-// and create a local user if success when enabled.
-func LoginViaPAM(login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
-	if err := pam.PAMAuth(cfg.ServiceName, login, password); err != nil {
-		if strings.Contains(err.Error(), "Authentication failure") {
-			return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-		}
-		return nil, err
-	}
-
-	if !autoRegister {
-		return nil, nil
-	}
-
-	user := &User{
-		LowerName:   strings.ToLower(login),
-		Name:        login,
-		Email:       login,
-		Passwd:      password,
-		LoginSource: sourceID,
-		LoginName:   login,
-		IsActive:    true,
-	}
-	return user, CreateUser(user)
-}
-
-// *************************
-// ----- GitHub config -----
-// *************************
-
-type GitHubConfig struct {
-	// the GitHub service endpoint, e.g. https://api.github.com/.
-	APIEndpoint string
-}
-
-func LoginViaGitHub(login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) {
-	fullname, email, url, location, err := github.Authenticate(cfg.APIEndpoint, login, password)
-	if err != nil {
-		if strings.Contains(err.Error(), "401") {
-			return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
-		}
-		return nil, err
-	}
-
-	if !autoRegister {
-		return nil, nil
-	}
-	user := &User{
-		LowerName:   strings.ToLower(login),
-		Name:        login,
-		FullName:    fullname,
-		Email:       email,
-		Website:     url,
-		Passwd:      password,
-		LoginSource: sourceID,
-		LoginName:   login,
-		IsActive:    true,
-		Location:    location,
-	}
-	return user, CreateUser(user)
-}
-
-func authenticateViaLoginSource(source *LoginSource, login, password string, autoRegister bool) (*User, error) {
-	if !source.IsActived {
-		return nil, errors.LoginSourceNotActivated{SourceID: source.ID}
-	}
-
-	switch source.Type {
-	case LoginLDAP, LoginDLDAP:
-		return LoginViaLDAP(login, password, source, autoRegister)
-	case LoginSMTP:
-		return LoginViaSMTP(login, password, source.ID, source.Config.(*SMTPConfig), autoRegister)
-	case LoginPAM:
-		return LoginViaPAM(login, password, source.ID, source.Config.(*PAMConfig), autoRegister)
-	case LoginGitHub:
-		return LoginViaGitHub(login, password, source.ID, source.Config.(*GitHubConfig), autoRegister)
-	}
-
-	return nil, errors.InvalidLoginSourceType{Type: source.Type}
-}

+ 46 - 14
internal/db/login_source_files.go

@@ -15,6 +15,11 @@ import (
 	"github.com/pkg/errors"
 	"gopkg.in/ini.v1"
 
+	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/auth/github"
+	"gogs.io/gogs/internal/auth/ldap"
+	"gogs.io/gogs/internal/auth/pam"
+	"gogs.io/gogs/internal/auth/smtp"
 	"gogs.io/gogs/internal/errutil"
 	"gogs.io/gogs/internal/osutil"
 )
@@ -154,30 +159,57 @@ func loadLoginSourceFiles(authdPath string, clock func() time.Time) (loginSource
 
 		// Parse authentication source file
 		authType := s.Key("type").String()
+		cfgSection := authSource.Section("config")
 		switch authType {
 		case "ldap_bind_dn":
-			loginSource.Type = LoginLDAP
-			loginSource.Config = &LDAPConfig{}
+			var cfg ldap.Config
+			err = cfgSection.MapTo(&cfg)
+			if err != nil {
+				return errors.Wrap(err, `map "config" section`)
+			}
+			loginSource.Type = auth.LDAP
+			loginSource.Provider = ldap.NewProvider(false, &cfg)
+
 		case "ldap_simple_auth":
-			loginSource.Type = LoginDLDAP
-			loginSource.Config = &LDAPConfig{}
+			var cfg ldap.Config
+			err = cfgSection.MapTo(&cfg)
+			if err != nil {
+				return errors.Wrap(err, `map "config" section`)
+			}
+			loginSource.Type = auth.DLDAP
+			loginSource.Provider = ldap.NewProvider(true, &cfg)
+
 		case "smtp":
-			loginSource.Type = LoginSMTP
-			loginSource.Config = &SMTPConfig{}
+			var cfg smtp.Config
+			err = cfgSection.MapTo(&cfg)
+			if err != nil {
+				return errors.Wrap(err, `map "config" section`)
+			}
+			loginSource.Type = auth.SMTP
+			loginSource.Provider = smtp.NewProvider(&cfg)
+
 		case "pam":
-			loginSource.Type = LoginPAM
-			loginSource.Config = &PAMConfig{}
+			var cfg pam.Config
+			err = cfgSection.MapTo(&cfg)
+			if err != nil {
+				return errors.Wrap(err, `map "config" section`)
+			}
+			loginSource.Type = auth.PAM
+			loginSource.Provider = pam.NewProvider(&cfg)
+
 		case "github":
-			loginSource.Type = LoginGitHub
-			loginSource.Config = &GitHubConfig{}
+			var cfg github.Config
+			err = cfgSection.MapTo(&cfg)
+			if err != nil {
+				return errors.Wrap(err, `map "config" section`)
+			}
+			loginSource.Type = auth.GitHub
+			loginSource.Provider = github.NewProvider(&cfg)
+
 		default:
 			return fmt.Errorf("unknown type %q", authType)
 		}
 
-		if err = authSource.Section("config").MapTo(loginSource.Config); err != nil {
-			return errors.Wrap(err, `map "config" section`)
-		}
-
 		store.sources = append(store.sources, loginSource)
 		return nil
 	})

+ 73 - 62
internal/db/login_sources.go

@@ -13,7 +13,11 @@ import (
 	"github.com/pkg/errors"
 	"gorm.io/gorm"
 
+	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/auth/github"
 	"gogs.io/gogs/internal/auth/ldap"
+	"gogs.io/gogs/internal/auth/pam"
+	"gogs.io/gogs/internal/auth/smtp"
 	"gogs.io/gogs/internal/errutil"
 )
 
@@ -46,12 +50,12 @@ var LoginSources LoginSourcesStore
 // LoginSource represents an external way for authorizing users.
 type LoginSource struct {
 	ID        int64
-	Type      LoginType
-	Name      string      `xorm:"UNIQUE" gorm:"UNIQUE"`
-	IsActived bool        `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL"`
-	IsDefault bool        `xorm:"DEFAULT false"`
-	Config    interface{} `xorm:"-" gorm:"-"`
-	RawConfig string      `xorm:"TEXT cfg" gorm:"COLUMN:cfg;TYPE:TEXT"`
+	Type      auth.Type
+	Name      string        `xorm:"UNIQUE" gorm:"UNIQUE"`
+	IsActived bool          `xorm:"NOT NULL DEFAULT false" gorm:"NOT NULL"`
+	IsDefault bool          `xorm:"DEFAULT false"`
+	Provider  auth.Provider `xorm:"-" gorm:"-"`
+	Config    string        `xorm:"TEXT cfg" gorm:"COLUMN:cfg;TYPE:TEXT" json:"RawConfig"`
 
 	Created     time.Time `xorm:"-" gorm:"-" json:"-"`
 	CreatedUnix int64
@@ -63,10 +67,10 @@ type LoginSource struct {
 
 // NOTE: This is a GORM save hook.
 func (s *LoginSource) BeforeSave(tx *gorm.DB) (err error) {
-	if s.Config == nil {
+	if s.Provider == nil {
 		return nil
 	}
-	s.RawConfig, err = jsoniter.MarshalToString(s.Config)
+	s.Config, err = jsoniter.MarshalToString(s.Provider.Config())
 	return err
 }
 
@@ -91,86 +95,90 @@ func (s *LoginSource) AfterFind(tx *gorm.DB) error {
 	s.Updated = time.Unix(s.UpdatedUnix, 0).Local()
 
 	switch s.Type {
-	case LoginLDAP, LoginDLDAP:
-		s.Config = new(LDAPConfig)
-	case LoginSMTP:
-		s.Config = new(SMTPConfig)
-	case LoginPAM:
-		s.Config = new(PAMConfig)
-	case LoginGitHub:
-		s.Config = new(GitHubConfig)
+	case auth.LDAP:
+		var cfg ldap.Config
+		err := jsoniter.UnmarshalFromString(s.Config, &cfg)
+		if err != nil {
+			return err
+		}
+		s.Provider = ldap.NewProvider(false, &cfg)
+
+	case auth.DLDAP:
+		var cfg ldap.Config
+		err := jsoniter.UnmarshalFromString(s.Config, &cfg)
+		if err != nil {
+			return err
+		}
+		s.Provider = ldap.NewProvider(true, &cfg)
+
+	case auth.SMTP:
+		var cfg smtp.Config
+		err := jsoniter.UnmarshalFromString(s.Config, &cfg)
+		if err != nil {
+			return err
+		}
+		s.Provider = smtp.NewProvider(&cfg)
+
+	case auth.PAM:
+		var cfg pam.Config
+		err := jsoniter.UnmarshalFromString(s.Config, &cfg)
+		if err != nil {
+			return err
+		}
+		s.Provider = pam.NewProvider(&cfg)
+
+	case auth.GitHub:
+		var cfg github.Config
+		err := jsoniter.UnmarshalFromString(s.Config, &cfg)
+		if err != nil {
+			return err
+		}
+		s.Provider = github.NewProvider(&cfg)
+
 	default:
 		return fmt.Errorf("unrecognized login source type: %v", s.Type)
 	}
-	return jsoniter.UnmarshalFromString(s.RawConfig, s.Config)
+	return nil
 }
 
 func (s *LoginSource) TypeName() string {
-	return LoginNames[s.Type]
+	return auth.Name(s.Type)
 }
 
 func (s *LoginSource) IsLDAP() bool {
-	return s.Type == LoginLDAP
+	return s.Type == auth.LDAP
 }
 
 func (s *LoginSource) IsDLDAP() bool {
-	return s.Type == LoginDLDAP
+	return s.Type == auth.DLDAP
 }
 
 func (s *LoginSource) IsSMTP() bool {
-	return s.Type == LoginSMTP
+	return s.Type == auth.SMTP
 }
 
 func (s *LoginSource) IsPAM() bool {
-	return s.Type == LoginPAM
+	return s.Type == auth.PAM
 }
 
 func (s *LoginSource) IsGitHub() bool {
-	return s.Type == LoginGitHub
-}
-
-func (s *LoginSource) HasTLS() bool {
-	return ((s.IsLDAP() || s.IsDLDAP()) &&
-		s.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) ||
-		s.IsSMTP()
+	return s.Type == auth.GitHub
 }
 
-func (s *LoginSource) UseTLS() bool {
-	switch s.Type {
-	case LoginLDAP, LoginDLDAP:
-		return s.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted
-	case LoginSMTP:
-		return s.SMTP().TLS
-	}
-
-	return false
+func (s *LoginSource) LDAP() *ldap.Config {
+	return s.Provider.Config().(*ldap.Config)
 }
 
-func (s *LoginSource) SkipVerify() bool {
-	switch s.Type {
-	case LoginLDAP, LoginDLDAP:
-		return s.LDAP().SkipVerify
-	case LoginSMTP:
-		return s.SMTP().SkipVerify
-	}
-
-	return false
+func (s *LoginSource) SMTP() *smtp.Config {
+	return s.Provider.Config().(*smtp.Config)
 }
 
-func (s *LoginSource) LDAP() *LDAPConfig {
-	return s.Config.(*LDAPConfig)
+func (s *LoginSource) PAM() *pam.Config {
+	return s.Provider.Config().(*pam.Config)
 }
 
-func (s *LoginSource) SMTP() *SMTPConfig {
-	return s.Config.(*SMTPConfig)
-}
-
-func (s *LoginSource) PAM() *PAMConfig {
-	return s.Config.(*PAMConfig)
-}
-
-func (s *LoginSource) GitHub() *GitHubConfig {
-	return s.Config.(*GitHubConfig)
+func (s *LoginSource) GitHub() *github.Config {
+	return s.Provider.Config().(*github.Config)
 }
 
 var _ LoginSourcesStore = (*loginSources)(nil)
@@ -181,7 +189,7 @@ type loginSources struct {
 }
 
 type CreateLoginSourceOpts struct {
-	Type      LoginType
+	Type      auth.Type
 	Name      string
 	Activated bool
 	Default   bool
@@ -214,7 +222,10 @@ func (db *loginSources) Create(opts CreateLoginSourceOpts) (*LoginSource, error)
 		Name:      opts.Name,
 		IsActived: opts.Activated,
 		IsDefault: opts.Default,
-		Config:    opts.Config,
+	}
+	source.Config, err = jsoniter.MarshalToString(opts.Config)
+	if err != nil {
+		return nil, err
 	}
 	return source, db.DB.Create(source).Error
 }
@@ -308,7 +319,7 @@ func (db *loginSources) Save(source *LoginSource) error {
 	source.File.SetGeneral("name", source.Name)
 	source.File.SetGeneral("is_activated", strconv.FormatBool(source.IsActived))
 	source.File.SetGeneral("is_default", strconv.FormatBool(source.IsDefault))
-	if err := source.File.SetConfig(source.Config); err != nil {
+	if err := source.File.SetConfig(source.Provider.Config()); err != nil {
 		return errors.Wrap(err, "set config")
 	} else if err = source.File.Save(); err != nil {
 		return errors.Wrap(err, "save file")

+ 35 - 28
internal/db/login_sources_test.go

@@ -11,6 +11,9 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gorm.io/gorm"
 
+	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/auth/github"
+	"gogs.io/gogs/internal/auth/pam"
 	"gogs.io/gogs/internal/errutil"
 )
 
@@ -30,18 +33,20 @@ func TestLoginSource_BeforeSave(t *testing.T) {
 		if err != nil {
 			t.Fatal(err)
 		}
-		assert.Empty(t, s.RawConfig)
+		assert.Empty(t, s.Config)
 	})
 
 	t.Run("Config has been set", func(t *testing.T) {
 		s := &LoginSource{
-			Config: &PAMConfig{ServiceName: "pam_service"},
+			Provider: pam.NewProvider(&pam.Config{
+				ServiceName: "pam_service",
+			}),
 		}
 		err := s.BeforeSave(db)
 		if err != nil {
 			t.Fatal(err)
 		}
-		assert.Equal(t, `{"ServiceName":"pam_service"}`, s.RawConfig)
+		assert.Equal(t, `{"ServiceName":"pam_service"}`, s.Config)
 	})
 }
 
@@ -109,11 +114,11 @@ func Test_loginSources(t *testing.T) {
 func test_loginSources_Create(t *testing.T, db *loginSources) {
 	// Create first login source with name "GitHub"
 	source, err := db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub",
 		Activated: true,
 		Default:   false,
-		Config: &GitHubConfig{
+		Config: &github.Config{
 			APIEndpoint: "https://api.github.com",
 		},
 	})
@@ -138,11 +143,11 @@ func test_loginSources_Create(t *testing.T, db *loginSources) {
 func test_loginSources_Count(t *testing.T, db *loginSources) {
 	// Create two login sources, one in database and one as source file.
 	_, err := db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub",
 		Activated: true,
 		Default:   false,
-		Config: &GitHubConfig{
+		Config: &github.Config{
 			APIEndpoint: "https://api.github.com",
 		},
 	})
@@ -162,11 +167,11 @@ func test_loginSources_Count(t *testing.T, db *loginSources) {
 func test_loginSources_DeleteByID(t *testing.T, db *loginSources) {
 	t.Run("delete but in used", func(t *testing.T) {
 		source, err := db.Create(CreateLoginSourceOpts{
-			Type:      LoginGitHub,
+			Type:      auth.GitHub,
 			Name:      "GitHub",
 			Activated: true,
 			Default:   false,
-			Config: &GitHubConfig{
+			Config: &github.Config{
 				APIEndpoint: "https://api.github.com",
 			},
 		})
@@ -175,8 +180,7 @@ func test_loginSources_DeleteByID(t *testing.T, db *loginSources) {
 		}
 
 		// Create a user that uses this login source
-		_, err = (&users{DB: db.DB}).Create(CreateUserOpts{
-			Name:        "alice",
+		_, err = (&users{DB: db.DB}).Create("alice", "", CreateUserOpts{
 			LoginSource: source.ID,
 		})
 		if err != nil {
@@ -197,11 +201,11 @@ func test_loginSources_DeleteByID(t *testing.T, db *loginSources) {
 
 	// Create a login source with name "GitHub2"
 	source, err := db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub2",
 		Activated: true,
 		Default:   false,
-		Config: &GitHubConfig{
+		Config: &github.Config{
 			APIEndpoint: "https://api.github.com",
 		},
 	})
@@ -243,13 +247,13 @@ func test_loginSources_GetByID(t *testing.T, db *loginSources) {
 		},
 	})
 
-	expConfig := &GitHubConfig{
+	expConfig := &github.Config{
 		APIEndpoint: "https://api.github.com",
 	}
 
 	// Create a login source with name "GitHub"
 	source, err := db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub",
 		Activated: true,
 		Default:   false,
@@ -264,7 +268,7 @@ func test_loginSources_GetByID(t *testing.T, db *loginSources) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	assert.Equal(t, expConfig, source.Config)
+	assert.Equal(t, expConfig, source.Provider.Config())
 
 	// Get the one in source file store
 	_, err = db.GetByID(101)
@@ -290,9 +294,9 @@ func test_loginSources_List(t *testing.T, db *loginSources) {
 
 	// Create two login sources in database, one activated and the other one not
 	_, err := db.Create(CreateLoginSourceOpts{
-		Type: LoginPAM,
+		Type: auth.PAM,
 		Name: "PAM",
-		Config: &PAMConfig{
+		Config: &pam.Config{
 			ServiceName: "PAM",
 		},
 	})
@@ -300,10 +304,10 @@ func test_loginSources_List(t *testing.T, db *loginSources) {
 		t.Fatal(err)
 	}
 	_, err = db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub",
 		Activated: true,
-		Config: &GitHubConfig{
+		Config: &github.Config{
 			APIEndpoint: "https://api.github.com",
 		},
 	})
@@ -348,10 +352,10 @@ func test_loginSources_ResetNonDefault(t *testing.T, db *loginSources) {
 
 	// Create two login sources both have default on
 	source1, err := db.Create(CreateLoginSourceOpts{
-		Type:    LoginPAM,
+		Type:    auth.PAM,
 		Name:    "PAM",
 		Default: true,
-		Config: &PAMConfig{
+		Config: &pam.Config{
 			ServiceName: "PAM",
 		},
 	})
@@ -359,11 +363,11 @@ func test_loginSources_ResetNonDefault(t *testing.T, db *loginSources) {
 		t.Fatal(err)
 	}
 	source2, err := db.Create(CreateLoginSourceOpts{
-		Type:      LoginGitHub,
+		Type:      auth.GitHub,
 		Name:      "GitHub",
 		Activated: true,
 		Default:   true,
-		Config: &GitHubConfig{
+		Config: &github.Config{
 			APIEndpoint: "https://api.github.com",
 		},
 	})
@@ -395,11 +399,11 @@ func test_loginSources_Save(t *testing.T, db *loginSources) {
 	t.Run("save to database", func(t *testing.T) {
 		// Create a login source with name "GitHub"
 		source, err := db.Create(CreateLoginSourceOpts{
-			Type:      LoginGitHub,
+			Type:      auth.GitHub,
 			Name:      "GitHub",
 			Activated: true,
 			Default:   false,
-			Config: &GitHubConfig{
+			Config: &github.Config{
 				APIEndpoint: "https://api.github.com",
 			},
 		})
@@ -408,9 +412,9 @@ func test_loginSources_Save(t *testing.T, db *loginSources) {
 		}
 
 		source.IsActived = false
-		source.Config = &GitHubConfig{
+		source.Provider = github.NewProvider(&github.Config{
 			APIEndpoint: "https://api2.github.com",
-		}
+		})
 		err = db.Save(source)
 		if err != nil {
 			t.Fatal(err)
@@ -427,6 +431,9 @@ func test_loginSources_Save(t *testing.T, db *loginSources) {
 	t.Run("save to file", func(t *testing.T) {
 		calledSave := false
 		source := &LoginSource{
+			Provider: github.NewProvider(&github.Config{
+				APIEndpoint: "https://api.github.com",
+			}),
 			File: &mockLoginSourceFileStore{
 				MockSetGeneral: func(name, value string) {},
 				MockSetConfig:  func(cfg interface{}) error { return nil },

+ 3 - 3
internal/db/mocks.go

@@ -209,7 +209,7 @@ var _ UsersStore = (*MockUsersStore)(nil)
 
 type MockUsersStore struct {
 	MockAuthenticate  func(username, password string, loginSourceID int64) (*User, error)
-	MockCreate        func(opts CreateUserOpts) (*User, error)
+	MockCreate        func(username, email string, opts CreateUserOpts) (*User, error)
 	MockGetByEmail    func(email string) (*User, error)
 	MockGetByID       func(id int64) (*User, error)
 	MockGetByUsername func(username string) (*User, error)
@@ -219,8 +219,8 @@ func (m *MockUsersStore) Authenticate(username, password string, loginSourceID i
 	return m.MockAuthenticate(username, password, loginSourceID)
 }
 
-func (m *MockUsersStore) Create(opts CreateUserOpts) (*User, error) {
-	return m.MockCreate(opts)
+func (m *MockUsersStore) Create(username, email string, opts CreateUserOpts) (*User, error) {
+	return m.MockCreate(username, email, opts)
 }
 
 func (m *MockUsersStore) GetByEmail(email string) (*User, error) {

+ 2 - 2
internal/db/testdata/backup/LoginSource.golden.json

@@ -1,2 +1,2 @@
-{"ID":1,"Type":4,"Name":"My PAM","IsActived":true,"IsDefault":false,"Config":null,"RawConfig":"{\"ServiceName\":\"PAM service\"}","CreatedUnix":1588568886,"UpdatedUnix":1588572486}
-{"ID":2,"Type":6,"Name":"GitHub.com","IsActived":true,"IsDefault":false,"Config":null,"RawConfig":"{\"APIEndpoint\":\"https://api.github.com\"}","CreatedUnix":1588568886,"UpdatedUnix":0}
+{"ID":1,"Type":4,"Name":"My PAM","IsActived":true,"IsDefault":false,"Provider":null,"RawConfig":"{\"ServiceName\":\"PAM service\"}","CreatedUnix":1588568886,"UpdatedUnix":1588572486}
+{"ID":2,"Type":6,"Name":"GitHub.com","IsActived":true,"IsDefault":false,"Provider":null,"RawConfig":"{\"APIEndpoint\":\"https://api.github.com\",\"SkipVerify\":false}","CreatedUnix":1588568886,"UpdatedUnix":0}

+ 62 - 32
internal/db/users.go

@@ -9,9 +9,11 @@ import (
 	"strings"
 	"time"
 
+	"github.com/go-macaron/binding"
 	"github.com/pkg/errors"
 	"gorm.io/gorm"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/cryptoutil"
 	"gogs.io/gogs/internal/errutil"
 )
@@ -32,10 +34,10 @@ type UsersStore interface {
 	// When the "loginSourceID" is positive, it tries to authenticate via given
 	// login source and creates a new user when not yet exists in the database.
 	Authenticate(username, password string, loginSourceID int64) (*User, error)
-	// Create creates a new user and persist to database.
+	// Create creates a new user and persists to database.
 	// It returns ErrUserAlreadyExist when a user with same name already exists,
 	// or ErrEmailAlreadyUsed if the email has been used by another user.
-	Create(opts CreateUserOpts) (*User, error)
+	Create(username, email string, opts CreateUserOpts) (*User, error)
 	// GetByEmail returns the user (not organization) with given email.
 	// It ignores records with unverified emails and returns ErrUserNotExist when not found.
 	GetByEmail(email string) (*User, error)
@@ -93,6 +95,9 @@ func (db *users) Authenticate(login, password string, loginSourceID int64) (*Use
 		return nil, errors.Wrap(err, "get user")
 	}
 
+	var authSourceID int64 // The login source ID will be used to authenticate the user
+	createNewUser := false // Whether to create a new user after successful authentication
+
 	// User found in the database
 	if err == nil {
 		// Note: This check is unnecessary but to reduce user confusion at login page
@@ -107,44 +112,64 @@ func (db *users) Authenticate(login, password string, loginSourceID int64) (*Use
 				return user, nil
 			}
 
-			return nil, ErrUserNotExist{args: map[string]interface{}{"userID": user.ID, "name": user.Name}}
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login, "userID": user.ID}}
 		}
 
-		source, err := LoginSources.GetByID(user.LoginSource)
-		if err != nil {
-			return nil, errors.Wrap(err, "get login source")
-		}
+		authSourceID = user.LoginSource
 
-		_, err = authenticateViaLoginSource(source, login, password, false)
-		if err != nil {
-			return nil, errors.Wrap(err, "authenticate via login source")
+	} else {
+		// Non-local login source is always greater than 0.
+		if loginSourceID <= 0 {
+			return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
 		}
-		return user, nil
-	}
 
-	// Non-local login source is always greater than 0.
-	if loginSourceID <= 0 {
-		return nil, ErrUserNotExist{args: map[string]interface{}{"login": login}}
+		authSourceID = loginSourceID
+		createNewUser = true
 	}
 
-	source, err := LoginSources.GetByID(loginSourceID)
+	source, err := LoginSources.GetByID(authSourceID)
 	if err != nil {
 		return nil, errors.Wrap(err, "get login source")
 	}
 
-	user, err = authenticateViaLoginSource(source, login, password, true)
+	if !source.IsActived {
+		return nil, errors.Errorf("login source %d is not activated", source.ID)
+	}
+
+	extAccount, err := source.Provider.Authenticate(login, password)
 	if err != nil {
-		return nil, errors.Wrap(err, "authenticate via login source")
+		return nil, err
 	}
-	return user, nil
+
+	if !createNewUser {
+		return user, nil
+	}
+
+	// Validate username make sure it satisfies requirement.
+	if binding.AlphaDashDotPattern.MatchString(extAccount.Name) {
+		return nil, fmt.Errorf("invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", extAccount.Name)
+	}
+
+	return Users.Create(extAccount.Name, extAccount.Email, CreateUserOpts{
+		FullName:    extAccount.FullName,
+		LoginSource: authSourceID,
+		LoginName:   extAccount.Login,
+		Location:    extAccount.Location,
+		Website:     extAccount.Website,
+		Activated:   true,
+		Admin:       extAccount.Admin,
+	})
 }
 
 type CreateUserOpts struct {
-	Name        string
-	Email       string
+	FullName    string
 	Password    string
 	LoginSource int64
+	LoginName   string
+	Location    string
+	Website     string
 	Activated   bool
+	Admin       bool
 }
 
 type ErrUserAlreadyExist struct {
@@ -181,36 +206,41 @@ func (err ErrEmailAlreadyUsed) Error() string {
 	return fmt.Sprintf("email has been used: %v", err.args)
 }
 
-func (db *users) Create(opts CreateUserOpts) (*User, error) {
-	err := isUsernameAllowed(opts.Name)
+func (db *users) Create(username, email string, opts CreateUserOpts) (*User, error) {
+	err := isUsernameAllowed(username)
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = db.GetByUsername(opts.Name)
+	_, err = db.GetByUsername(username)
 	if err == nil {
-		return nil, ErrUserAlreadyExist{args: errutil.Args{"name": opts.Name}}
+		return nil, ErrUserAlreadyExist{args: errutil.Args{"name": username}}
 	} else if !IsErrUserNotExist(err) {
 		return nil, err
 	}
 
-	_, err = db.GetByEmail(opts.Email)
+	_, err = db.GetByEmail(email)
 	if err == nil {
-		return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": opts.Email}}
+		return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": email}}
 	} else if !IsErrUserNotExist(err) {
 		return nil, err
 	}
 
 	user := &User{
-		LowerName:       strings.ToLower(opts.Name),
-		Name:            opts.Name,
-		Email:           opts.Email,
+		LowerName:       strings.ToLower(username),
+		Name:            username,
+		FullName:        opts.FullName,
+		Email:           email,
 		Passwd:          opts.Password,
 		LoginSource:     opts.LoginSource,
+		LoginName:       opts.LoginName,
+		Location:        opts.Location,
+		Website:         opts.Website,
 		MaxRepoCreation: -1,
 		IsActive:        opts.Activated,
-		Avatar:          cryptoutil.MD5(opts.Email),
-		AvatarEmail:     opts.Email,
+		IsAdmin:         opts.Admin,
+		Avatar:          cryptoutil.MD5(email),
+		AvatarEmail:     email,
 	}
 
 	user.Rands, err = GetUserSalt()

+ 13 - 38
internal/db/users_test.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/errutil"
 )
 
@@ -51,9 +52,7 @@ func Test_users(t *testing.T) {
 //  along with addressing https://github.com/gogs/gogs/issues/6115.
 func test_users_Authenticate(t *testing.T, db *users) {
 	password := "pa$$word"
-	alice, err := db.Create(CreateUserOpts{
-		Name:     "alice",
-		Email:    "[email protected]",
+	alice, err := db.Create("alice", "[email protected]", CreateUserOpts{
 		Password: password,
 	})
 	if err != nil {
@@ -62,13 +61,13 @@ func test_users_Authenticate(t *testing.T, db *users) {
 
 	t.Run("user not found", func(t *testing.T) {
 		_, err := db.Authenticate("bob", password, -1)
-		expErr := ErrUserNotExist{args: map[string]interface{}{"login": "bob"}}
+		expErr := auth.ErrBadCredentials{Args: map[string]interface{}{"login": "bob"}}
 		assert.Equal(t, expErr, err)
 	})
 
 	t.Run("invalid password", func(t *testing.T) {
 		_, err := db.Authenticate(alice.Name, "bad_password", -1)
-		expErr := ErrUserNotExist{args: map[string]interface{}{"userID": alice.ID, "name": alice.Name}}
+		expErr := auth.ErrBadCredentials{Args: map[string]interface{}{"login": alice.Name, "userID": alice.ID}}
 		assert.Equal(t, expErr, err)
 	})
 
@@ -90,9 +89,7 @@ func test_users_Authenticate(t *testing.T, db *users) {
 }
 
 func test_users_Create(t *testing.T, db *users) {
-	alice, err := db.Create(CreateUserOpts{
-		Name:      "alice",
-		Email:     "[email protected]",
+	alice, err := db.Create("alice", "[email protected]", CreateUserOpts{
 		Activated: true,
 	})
 	if err != nil {
@@ -100,26 +97,19 @@ func test_users_Create(t *testing.T, db *users) {
 	}
 
 	t.Run("name not allowed", func(t *testing.T) {
-		_, err := db.Create(CreateUserOpts{
-			Name: "-",
-		})
+		_, err := db.Create("-", "", CreateUserOpts{})
 		expErr := ErrNameNotAllowed{args: errutil.Args{"reason": "reserved", "name": "-"}}
 		assert.Equal(t, expErr, err)
 	})
 
 	t.Run("name already exists", func(t *testing.T) {
-		_, err := db.Create(CreateUserOpts{
-			Name: alice.Name,
-		})
+		_, err := db.Create(alice.Name, "", CreateUserOpts{})
 		expErr := ErrUserAlreadyExist{args: errutil.Args{"name": alice.Name}}
 		assert.Equal(t, expErr, err)
 	})
 
 	t.Run("email already exists", func(t *testing.T) {
-		_, err := db.Create(CreateUserOpts{
-			Name:  "bob",
-			Email: alice.Email,
-		})
+		_, err := db.Create("bob", alice.Email, CreateUserOpts{})
 		expErr := ErrEmailAlreadyUsed{args: errutil.Args{"email": alice.Email}}
 		assert.Equal(t, expErr, err)
 	})
@@ -141,10 +131,7 @@ func test_users_GetByEmail(t *testing.T, db *users) {
 
 	t.Run("ignore organization", func(t *testing.T) {
 		// TODO: Use Orgs.Create to replace SQL hack when the method is available.
-		org, err := db.Create(CreateUserOpts{
-			Name:  "gogs",
-			Email: "[email protected]",
-		})
+		org, err := db.Create("gogs", "[email protected]", CreateUserOpts{})
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -160,10 +147,7 @@ func test_users_GetByEmail(t *testing.T, db *users) {
 	})
 
 	t.Run("by primary email", func(t *testing.T) {
-		alice, err := db.Create(CreateUserOpts{
-			Name:  "alice",
-			Email: "[email protected]",
-		})
+		alice, err := db.Create("alice", "[email protected]", CreateUserOpts{})
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -187,10 +171,7 @@ func test_users_GetByEmail(t *testing.T, db *users) {
 	})
 
 	t.Run("by secondary email", func(t *testing.T) {
-		bob, err := db.Create(CreateUserOpts{
-			Name:  "bob",
-			Email: "[email protected]",
-		})
+		bob, err := db.Create("bob", "[email protected]", CreateUserOpts{})
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -221,10 +202,7 @@ func test_users_GetByEmail(t *testing.T, db *users) {
 }
 
 func test_users_GetByID(t *testing.T, db *users) {
-	alice, err := db.Create(CreateUserOpts{
-		Name:  "alice",
-		Email: "[email protected]",
-	})
+	alice, err := db.Create("alice", "[email protected]", CreateUserOpts{})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -241,10 +219,7 @@ func test_users_GetByID(t *testing.T, db *users) {
 }
 
 func test_users_GetByUsername(t *testing.T, db *users) {
-	alice, err := db.Create(CreateUserOpts{
-		Name:  "alice",
-		Email: "[email protected]",
-	})
+	alice, err := db.Create("alice", "[email protected]", CreateUserOpts{})
 	if err != nil {
 		t.Fatal(err)
 	}

+ 73 - 66
internal/route/admin/auths.go

@@ -12,7 +12,11 @@ import (
 	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/auth/github"
 	"gogs.io/gogs/internal/auth/ldap"
+	"gogs.io/gogs/internal/auth/pam"
+	"gogs.io/gogs/internal/auth/smtp"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
@@ -48,16 +52,16 @@ type dropdownItem struct {
 
 var (
 	authSources = []dropdownItem{
-		{db.LoginNames[db.LoginLDAP], db.LoginLDAP},
-		{db.LoginNames[db.LoginDLDAP], db.LoginDLDAP},
-		{db.LoginNames[db.LoginSMTP], db.LoginSMTP},
-		{db.LoginNames[db.LoginPAM], db.LoginPAM},
-		{db.LoginNames[db.LoginGitHub], db.LoginGitHub},
+		{auth.Name(auth.LDAP), auth.LDAP},
+		{auth.Name(auth.DLDAP), auth.DLDAP},
+		{auth.Name(auth.SMTP), auth.SMTP},
+		{auth.Name(auth.PAM), auth.PAM},
+		{auth.Name(auth.GitHub), auth.GitHub},
 	}
 	securityProtocols = []dropdownItem{
-		{db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
-		{db.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
-		{db.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
+		{ldap.SecurityProtocolName(ldap.SecurityProtocolUnencrypted), ldap.SecurityProtocolUnencrypted},
+		{ldap.SecurityProtocolName(ldap.SecurityProtocolLDAPS), ldap.SecurityProtocolLDAPS},
+		{ldap.SecurityProtocolName(ldap.SecurityProtocolStartTLS), ldap.SecurityProtocolStartTLS},
 	}
 )
 
@@ -66,47 +70,45 @@ func NewAuthSource(c *context.Context) {
 	c.PageIs("Admin")
 	c.PageIs("AdminAuthentications")
 
-	c.Data["type"] = db.LoginLDAP
-	c.Data["CurrentTypeName"] = db.LoginNames[db.LoginLDAP]
-	c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
+	c.Data["type"] = auth.LDAP
+	c.Data["CurrentTypeName"] = auth.Name(auth.LDAP)
+	c.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolName(ldap.SecurityProtocolUnencrypted)
 	c.Data["smtp_auth"] = "PLAIN"
 	c.Data["is_active"] = true
 	c.Data["is_default"] = true
 	c.Data["AuthSources"] = authSources
 	c.Data["SecurityProtocols"] = securityProtocols
-	c.Data["SMTPAuths"] = db.SMTPAuths
+	c.Data["SMTPAuths"] = smtp.AuthTypes
 	c.Success(AUTH_NEW)
 }
 
-func parseLDAPConfig(f form.Authentication) *db.LDAPConfig {
-	return &db.LDAPConfig{
-		Source: ldap.Source{
-			Host:              f.Host,
-			Port:              f.Port,
-			SecurityProtocol:  ldap.SecurityProtocol(f.SecurityProtocol),
-			SkipVerify:        f.SkipVerify,
-			BindDN:            f.BindDN,
-			UserDN:            f.UserDN,
-			BindPassword:      f.BindPassword,
-			UserBase:          f.UserBase,
-			AttributeUsername: f.AttributeUsername,
-			AttributeName:     f.AttributeName,
-			AttributeSurname:  f.AttributeSurname,
-			AttributeMail:     f.AttributeMail,
-			AttributesInBind:  f.AttributesInBind,
-			Filter:            f.Filter,
-			GroupEnabled:      f.GroupEnabled,
-			GroupDN:           f.GroupDN,
-			GroupFilter:       f.GroupFilter,
-			GroupMemberUID:    f.GroupMemberUID,
-			UserUID:           f.UserUID,
-			AdminFilter:       f.AdminFilter,
-		},
+func parseLDAPConfig(f form.Authentication) *ldap.Config {
+	return &ldap.Config{
+		Host:              f.Host,
+		Port:              f.Port,
+		SecurityProtocol:  ldap.SecurityProtocol(f.SecurityProtocol),
+		SkipVerify:        f.SkipVerify,
+		BindDN:            f.BindDN,
+		UserDN:            f.UserDN,
+		BindPassword:      f.BindPassword,
+		UserBase:          f.UserBase,
+		AttributeUsername: f.AttributeUsername,
+		AttributeName:     f.AttributeName,
+		AttributeSurname:  f.AttributeSurname,
+		AttributeMail:     f.AttributeMail,
+		AttributesInBind:  f.AttributesInBind,
+		Filter:            f.Filter,
+		GroupEnabled:      f.GroupEnabled,
+		GroupDN:           f.GroupDN,
+		GroupFilter:       f.GroupFilter,
+		GroupMemberUID:    f.GroupMemberUID,
+		UserUID:           f.UserUID,
+		AdminFilter:       f.AdminFilter,
 	}
 }
 
-func parseSMTPConfig(f form.Authentication) *db.SMTPConfig {
-	return &db.SMTPConfig{
+func parseSMTPConfig(f form.Authentication) *smtp.Config {
+	return &smtp.Config{
 		Auth:           f.SMTPAuth,
 		Host:           f.SMTPHost,
 		Port:           f.SMTPPort,
@@ -121,29 +123,31 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 	c.PageIs("Admin")
 	c.PageIs("AdminAuthentications")
 
-	c.Data["CurrentTypeName"] = db.LoginNames[db.LoginType(f.Type)]
-	c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocol(f.SecurityProtocol)]
+	c.Data["CurrentTypeName"] = auth.Name(auth.Type(f.Type))
+	c.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolName(ldap.SecurityProtocol(f.SecurityProtocol))
 	c.Data["AuthSources"] = authSources
 	c.Data["SecurityProtocols"] = securityProtocols
-	c.Data["SMTPAuths"] = db.SMTPAuths
+	c.Data["SMTPAuths"] = smtp.AuthTypes
 
 	hasTLS := false
 	var config interface{}
-	switch db.LoginType(f.Type) {
-	case db.LoginLDAP, db.LoginDLDAP:
+	switch auth.Type(f.Type) {
+	case auth.LDAP, auth.DLDAP:
 		config = parseLDAPConfig(f)
 		hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
-	case db.LoginSMTP:
+	case auth.SMTP:
 		config = parseSMTPConfig(f)
 		hasTLS = true
-	case db.LoginPAM:
-		config = &db.PAMConfig{
+	case auth.PAM:
+		config = &pam.Config{
 			ServiceName: f.PAMServiceName,
 		}
-	case db.LoginGitHub:
-		config = &db.GitHubConfig{
+	case auth.GitHub:
+		config = &github.Config{
 			APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
+			SkipVerify:  f.SkipVerify,
 		}
+		hasTLS = true
 	default:
 		c.Status(http.StatusBadRequest)
 		return
@@ -156,7 +160,7 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
 	}
 
 	source, err := db.LoginSources.Create(db.CreateLoginSourceOpts{
-		Type:      db.LoginType(f.Type),
+		Type:      auth.Type(f.Type),
 		Name:      f.Name,
 		Activated: f.IsActive,
 		Default:   f.IsDefault,
@@ -192,7 +196,7 @@ func EditAuthSource(c *context.Context) {
 	c.PageIs("AdminAuthentications")
 
 	c.Data["SecurityProtocols"] = securityProtocols
-	c.Data["SMTPAuths"] = db.SMTPAuths
+	c.Data["SMTPAuths"] = smtp.AuthTypes
 
 	source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
 	if err != nil {
@@ -200,7 +204,7 @@ func EditAuthSource(c *context.Context) {
 		return
 	}
 	c.Data["Source"] = source
-	c.Data["HasTLS"] = source.HasTLS()
+	c.Data["HasTLS"] = source.Provider.HasTLS()
 
 	c.Success(AUTH_EDIT)
 }
@@ -210,7 +214,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 	c.PageIs("Admin")
 	c.PageIs("AdminAuthentications")
 
-	c.Data["SMTPAuths"] = db.SMTPAuths
+	c.Data["SMTPAuths"] = smtp.AuthTypes
 
 	source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
 	if err != nil {
@@ -218,27 +222,30 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 		return
 	}
 	c.Data["Source"] = source
-	c.Data["HasTLS"] = source.HasTLS()
+	c.Data["HasTLS"] = source.Provider.HasTLS()
 
 	if c.HasError() {
 		c.Success(AUTH_EDIT)
 		return
 	}
 
-	var config interface{}
-	switch db.LoginType(f.Type) {
-	case db.LoginLDAP, db.LoginDLDAP:
-		config = parseLDAPConfig(f)
-	case db.LoginSMTP:
-		config = parseSMTPConfig(f)
-	case db.LoginPAM:
-		config = &db.PAMConfig{
+	var provider auth.Provider
+	switch auth.Type(f.Type) {
+	case auth.LDAP:
+		provider = ldap.NewProvider(false, parseLDAPConfig(f))
+	case auth.DLDAP:
+		provider = ldap.NewProvider(true, parseLDAPConfig(f))
+	case auth.SMTP:
+		provider = smtp.NewProvider(parseSMTPConfig(f))
+	case auth.PAM:
+		provider = pam.NewProvider(&pam.Config{
 			ServiceName: f.PAMServiceName,
-		}
-	case db.LoginGitHub:
-		config = &db.GitHubConfig{
+		})
+	case auth.GitHub:
+		provider = github.NewProvider(&github.Config{
 			APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
-		}
+			SkipVerify:  f.SkipVerify,
+		})
 	default:
 		c.Status(http.StatusBadRequest)
 		return
@@ -247,7 +254,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
 	source.Name = f.Name
 	source.IsActived = f.IsActive
 	source.IsDefault = f.IsDefault
-	source.Config = config
+	source.Provider = provider
 	if err := db.LoginSources.Save(source); err != nil {
 		c.Error(err, "update login source")
 		return

+ 3 - 2
internal/route/lfs/route.go

@@ -11,6 +11,7 @@ import (
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/authutil"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db"
@@ -59,7 +60,7 @@ func authenticate() macaron.Handler {
 		}
 
 		user, err := db.Users.Authenticate(username, password, -1)
-		if err != nil && !db.IsErrUserNotExist(err) {
+		if err != nil && !auth.IsErrBadCredentials(err) {
 			internalServerError(c.Resp)
 			log.Error("Failed to authenticate user [name: %s]: %v", username, err)
 			return
@@ -71,7 +72,7 @@ func authenticate() macaron.Handler {
 		}
 
 		// If username and password authentication failed, try again using username as an access token.
-		if db.IsErrUserNotExist(err) {
+		if auth.IsErrBadCredentials(err) {
 			token, err := db.AccessTokens.GetBySHA(username)
 			if err != nil {
 				if db.IsErrAccessTokenNotExist(err) {

+ 3 - 2
internal/route/lfs/route_test.go

@@ -14,6 +14,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/macaron.v1"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/lfsutil"
 )
@@ -70,7 +71,7 @@ func Test_authenticate(t *testing.T) {
 			},
 			mockUsersStore: &db.MockUsersStore{
 				MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
-					return nil, db.ErrUserNotExist{}
+					return nil, auth.ErrBadCredentials{}
 				},
 			},
 			mockAccessTokensStore: &db.MockAccessTokensStore{
@@ -112,7 +113,7 @@ func Test_authenticate(t *testing.T) {
 			},
 			mockUsersStore: &db.MockUsersStore{
 				MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
-					return nil, db.ErrUserNotExist{}
+					return nil, auth.ErrBadCredentials{}
 				},
 				MockGetByID: func(id int64) (*db.User, error) {
 					return &db.User{ID: 1, Name: "unknwon"}, nil

+ 2 - 1
internal/route/org/setting.go

@@ -9,6 +9,7 @@ import (
 
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
@@ -109,7 +110,7 @@ func SettingsDelete(c *context.Context) {
 	org := c.Org.Organization
 	if c.Req.Method == "POST" {
 		if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
-			if db.IsErrUserNotExist(err) {
+			if auth.IsErrBadCredentials(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {
 				c.Error(err, "authenticate user")

+ 2 - 1
internal/route/repo/http.go

@@ -20,6 +20,7 @@ import (
 	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/lazyregexp"
@@ -122,7 +123,7 @@ func HTTPContexter() macaron.Handler {
 		}
 
 		authUser, err := db.Users.Authenticate(authUsername, authPassword, -1)
-		if err != nil && !db.IsErrUserNotExist(err) {
+		if err != nil && !auth.IsErrBadCredentials(err) {
 			c.Status(http.StatusInternalServerError)
 			log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)
 			return

+ 2 - 1
internal/route/user/auth.go

@@ -12,6 +12,7 @@ import (
 	"github.com/pkg/errors"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
@@ -163,7 +164,7 @@ func LoginPost(c *context.Context, f form.SignIn) {
 	u, err := db.Users.Authenticate(f.UserName, f.Password, f.LoginSource)
 	if err != nil {
 		switch errors.Cause(err).(type) {
-		case db.ErrUserNotExist:
+		case auth.ErrBadCredentials:
 			c.FormErr("UserName", "Password")
 			c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
 		case db.ErrLoginSourceMismatch:

+ 2 - 1
internal/route/user/setting.go

@@ -18,6 +18,7 @@ import (
 	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/auth"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/cryptoutil"
@@ -640,7 +641,7 @@ func SettingsDelete(c *context.Context) {
 
 	if c.Req.Method == "POST" {
 		if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
-			if db.IsErrUserNotExist(err) {
+			if auth.IsErrBadCredentials(err) {
 				c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
 			} else {
 				c.Errorf(err, "authenticate user")

+ 1 - 0
public/js/gogs.js

@@ -1089,6 +1089,7 @@ function initAdmin() {
           break;
         case "6": //GITHUB
           $(".github").show();
+          $(".has-tls").show();
           break;
       }
 

+ 2 - 2
templates/admin/auth/edit.tmpl

@@ -180,13 +180,13 @@
 						<div class="inline field {{if not .Source.IsSMTP}}hide{{end}}">
 							<div class="ui checkbox">
 								<label><strong>{{.i18n.Tr "admin.auths.enable_tls"}}</strong></label>
-								<input name="tls" type="checkbox" {{if .Source.UseTLS}}checked{{end}}>
+								<input name="tls" type="checkbox" {{if .Source.Provider.UseTLS}}checked{{end}}>
 							</div>
 						</div>
 						<div class="has-tls inline field {{if not .HasTLS}}hide{{end}}">
 							<div class="ui checkbox">
 								<label><strong>{{.i18n.Tr "admin.auths.skip_tls_verify"}}</strong></label>
-								<input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}>
+								<input name="skip_verify" type="checkbox" {{if .Source.Provider.SkipTLSVerify}}checked{{end}}>
 							</div>
 						</div>
 						<div class="inline field">

Some files were not shown because too many files changed in this diff