From 05e09d910a34118406d7252d873696a47c80a3a6 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Fri, 18 Jun 2021 21:31:21 +0100
Subject: [PATCH 01/44] Rename auth.Auth auth.Method

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 modules/context/api.go        | 2 +-
 modules/context/context.go    | 2 +-
 services/auth/auth.go         | 6 +++---
 services/auth/basic.go        | 2 +-
 services/auth/group.go        | 6 +++---
 services/auth/interface.go    | 4 ++--
 services/auth/oauth2.go       | 2 +-
 services/auth/reverseproxy.go | 2 +-
 services/auth/session.go      | 2 +-
 services/auth/sspi_windows.go | 2 +-
 10 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/modules/context/api.go b/modules/context/api.go
index 506824674522e..78d48e9169be5 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -218,7 +218,7 @@ func (ctx *APIContext) CheckForOTP() {
 }
 
 // APIAuth converts auth.Auth as a middleware
-func APIAuth(authMethod auth.Auth) func(*APIContext) {
+func APIAuth(authMethod auth.Method) func(*APIContext) {
 	return func(ctx *APIContext) {
 		// Get user from session if logged in.
 		ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
diff --git a/modules/context/context.go b/modules/context/context.go
index 7b3fd2899acd9..f1bc4f3ab2557 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -612,7 +612,7 @@ func getCsrfOpts() CsrfOptions {
 }
 
 // Auth converts auth.Auth as a middleware
-func Auth(authMethod auth.Auth) func(*Context) {
+func Auth(authMethod auth.Method) func(*Context) {
 	return func(ctx *Context) {
 		ctx.User = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
 		if ctx.User != nil {
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 5492a8b74ede3..1dedfbf779b58 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -27,7 +27,7 @@ import (
 //
 // The Session plugin is expected to be executed second, in order to skip authentication
 // for users that have already signed in.
-var authMethods = []Auth{
+var authMethods = []Method{
 	&OAuth2{},
 	&Basic{},
 	&Session{},
@@ -40,12 +40,12 @@ var (
 )
 
 // Methods returns the instances of all registered methods
-func Methods() []Auth {
+func Methods() []Method {
 	return authMethods
 }
 
 // Register adds the specified instance to the list of available methods
-func Register(method Auth) {
+func Register(method Method) {
 	authMethods = append(authMethods, method)
 }
 
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 0bce4f1d067a2..5516eaffc810e 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -19,7 +19,7 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Basic{}
+	_ Method = &Basic{}
 )
 
 // Basic implements the Auth interface and authenticates requests (API requests
diff --git a/services/auth/group.go b/services/auth/group.go
index b61949de7dea7..7e887cfa8b58e 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -12,16 +12,16 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Group{}
+	_ Method = &Group{}
 )
 
 // Group implements the Auth interface with serval Auth.
 type Group struct {
-	methods []Auth
+	methods []Method
 }
 
 // NewGroup creates a new auth group
-func NewGroup(methods ...Auth) *Group {
+func NewGroup(methods ...Method) *Group {
 	return &Group{
 		methods: methods,
 	}
diff --git a/services/auth/interface.go b/services/auth/interface.go
index a305bdfc226c7..e75a84677c2b1 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -18,8 +18,8 @@ type DataStore middleware.DataStore
 // SessionStore represents a session store
 type SessionStore session.Store
 
-// Auth represents an authentication method (plugin) for HTTP requests.
-type Auth interface {
+// Method represents an authentication method (plugin) for HTTP requests.
+type Method interface {
 	Name() string
 
 	// Init should be called exactly once before using any of the other methods,
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index c6b98c144f0ef..e9f4c69e8886d 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -18,7 +18,7 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &OAuth2{}
+	_ Method = &OAuth2{}
 )
 
 // CheckOAuthAccessToken returns uid of user from oauth token
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index f958d28c9a664..90d830846e165 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -19,7 +19,7 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &ReverseProxy{}
+	_ Method = &ReverseProxy{}
 )
 
 // ReverseProxy implements the Auth interface, but actually relies on
diff --git a/services/auth/session.go b/services/auth/session.go
index 9f08f43363009..c3fcbc2bda67c 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -13,7 +13,7 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Session{}
+	_ Method = &Session{}
 )
 
 // Session checks if there is a user uid stored in the session and returns the user
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go
index d1289a76174b2..01243123081a5 100644
--- a/services/auth/sspi_windows.go
+++ b/services/auth/sspi_windows.go
@@ -32,7 +32,7 @@ var (
 	sspiAuth *websspi.Authenticator
 
 	// Ensure the struct implements the interface.
-	_ Auth = &SSPI{}
+	_ Method = &SSPI{}
 )
 
 // SSPI implements the SingleSignOn interface and authenticates requests

From dcddf7dae31abc4e66d1808d6814947384d6f84c Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Fri, 18 Jun 2021 21:47:12 +0100
Subject: [PATCH 02/44] Move UserSignIn and ExternalLogin in to services

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go  | 127 +++-----------------------------------
 services/auth/basic.go  |   2 +-
 services/auth/signin.go | 133 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 141 insertions(+), 121 deletions(-)
 create mode 100644 services/auth/signin.go

diff --git a/models/login_source.go b/models/login_source.go
index 098b48a8cd5f4..42bc0f72b194c 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -376,6 +376,13 @@ func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
 // ActiveLoginSources returns all active sources of the specified type
 func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 1)
+	if loginType < 0 {
+		if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
+			return nil, err
+		}
+		return sources, nil
+	}
+
 	if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
 		return nil, err
 	}
@@ -741,123 +748,3 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 	}
 	return user, CreateUser(user)
 }
-
-// ExternalUserLogin attempts a login using external source types.
-func ExternalUserLogin(user *User, login, password string, source *LoginSource) (*User, error) {
-	if !source.IsActived {
-		return nil, ErrLoginSourceNotActived
-	}
-
-	var err error
-	switch source.Type {
-	case LoginLDAP, LoginDLDAP:
-		user, err = LoginViaLDAP(user, login, password, source)
-	case LoginSMTP:
-		user, err = LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig))
-	case LoginPAM:
-		user, err = LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig))
-	default:
-		return nil, ErrUnsupportedLoginType
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
-	// user could be hint to resend confirm email.
-	if user.ProhibitLogin {
-		return nil, ErrUserProhibitLogin{user.ID, user.Name}
-	}
-
-	return user, nil
-}
-
-// UserSignIn validates user name and password.
-func UserSignIn(username, password string) (*User, error) {
-	var user *User
-	if strings.Contains(username, "@") {
-		user = &User{Email: strings.ToLower(strings.TrimSpace(username))}
-		// check same email
-		cnt, err := x.Count(user)
-		if err != nil {
-			return nil, err
-		}
-		if cnt > 1 {
-			return nil, ErrEmailAlreadyUsed{
-				Email: user.Email,
-			}
-		}
-	} else {
-		trimmedUsername := strings.TrimSpace(username)
-		if len(trimmedUsername) == 0 {
-			return nil, ErrUserNotExist{0, username, 0}
-		}
-
-		user = &User{LowerName: strings.ToLower(trimmedUsername)}
-	}
-
-	hasUser, err := x.Get(user)
-	if err != nil {
-		return nil, err
-	}
-
-	if hasUser {
-		switch user.LoginType {
-		case LoginNoType, LoginPlain, LoginOAuth2:
-			if user.IsPasswordSet() && user.ValidatePassword(password) {
-
-				// Update password hash if server password hash algorithm have changed
-				if user.PasswdHashAlgo != setting.PasswordHashAlgo {
-					if err = user.SetPassword(password); err != nil {
-						return nil, err
-					}
-					if err = UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
-						return nil, err
-					}
-				}
-
-				// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
-				// user could be hint to resend confirm email.
-				if user.ProhibitLogin {
-					return nil, ErrUserProhibitLogin{user.ID, user.Name}
-				}
-
-				return user, nil
-			}
-
-			return nil, ErrUserNotExist{user.ID, user.Name, 0}
-
-		default:
-			var source LoginSource
-			hasSource, err := x.ID(user.LoginSource).Get(&source)
-			if err != nil {
-				return nil, err
-			} else if !hasSource {
-				return nil, ErrLoginSourceNotExist{user.LoginSource}
-			}
-
-			return ExternalUserLogin(user, user.LoginName, password, &source)
-		}
-	}
-
-	sources := make([]*LoginSource, 0, 5)
-	if err = x.Where("is_actived = ?", true).Find(&sources); err != nil {
-		return nil, err
-	}
-
-	for _, source := range sources {
-		if source.IsOAuth2() || source.IsSSPI() {
-			// don't try to authenticate against OAuth2 and SSPI sources here
-			continue
-		}
-		authUser, err := ExternalUserLogin(nil, username, password, source)
-		if err == nil {
-			return authUser, nil
-		}
-
-		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
-	}
-
-	return nil, ErrUserNotExist{user.ID, user.Name, 0}
-}
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 5516eaffc810e..f0486f905c379 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -116,7 +116,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
 	}
 
 	log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
-	u, err := models.UserSignIn(uname, passwd)
+	u, err := UserSignIn(uname, passwd)
 	if err != nil {
 		if !models.IsErrUserNotExist(err) {
 			log.Error("UserSignIn: %v", err)
diff --git a/services/auth/signin.go b/services/auth/signin.go
new file mode 100644
index 0000000000000..8a0c16eddfad8
--- /dev/null
+++ b/services/auth/signin.go
@@ -0,0 +1,133 @@
+// Copyright 2021 The Gitea 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 auth
+
+import (
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// UserSignIn validates user name and password.
+func UserSignIn(username, password string) (*models.User, error) {
+	var user *models.User
+	if strings.Contains(username, "@") {
+		user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))}
+		// check same email
+		cnt, err := models.Count(user)
+		if err != nil {
+			return nil, err
+		}
+		if cnt > 1 {
+			return nil, models.ErrEmailAlreadyUsed{
+				Email: user.Email,
+			}
+		}
+	} else {
+		trimmedUsername := strings.TrimSpace(username)
+		if len(trimmedUsername) == 0 {
+			return nil, models.ErrUserNotExist{Name: username}
+		}
+
+		user = &models.User{LowerName: strings.ToLower(trimmedUsername)}
+	}
+
+	hasUser, err := models.GetUser(user)
+	if err != nil {
+		return nil, err
+	}
+
+	if hasUser {
+		switch user.LoginType {
+		case models.LoginNoType, models.LoginPlain, models.LoginOAuth2:
+			if user.IsPasswordSet() && user.ValidatePassword(password) {
+
+				// Update password hash if server password hash algorithm have changed
+				if user.PasswdHashAlgo != setting.PasswordHashAlgo {
+					if err = user.SetPassword(password); err != nil {
+						return nil, err
+					}
+					if err = models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
+						return nil, err
+					}
+				}
+
+				// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
+				// user could be hint to resend confirm email.
+				if user.ProhibitLogin {
+					return nil, models.ErrUserProhibitLogin{
+						UID:  user.ID,
+						Name: user.Name,
+					}
+				}
+
+				return user, nil
+			}
+
+			return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
+
+		default:
+			source, err := models.GetLoginSourceByID(user.LoginSource)
+			if err != nil {
+				return nil, err
+			}
+
+			return ExternalUserLogin(user, user.LoginName, password, source)
+		}
+	}
+
+	sources, err := models.ActiveLoginSources(-1)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, source := range sources {
+		if source.IsOAuth2() || source.IsSSPI() {
+			// don't try to authenticate against OAuth2 and SSPI sources here
+			continue
+		}
+		authUser, err := ExternalUserLogin(nil, username, password, source)
+		if err == nil {
+			return authUser, nil
+		}
+
+		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+	}
+
+	return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
+}
+
+// ExternalUserLogin attempts a login using external source types.
+func ExternalUserLogin(user *models.User, login, password string, source *models.LoginSource) (*models.User, error) {
+	if !source.IsActived {
+		return nil, models.ErrLoginSourceNotActived
+	}
+
+	var err error
+	switch source.Type {
+	case models.LoginLDAP, models.LoginDLDAP:
+		user, err = models.LoginViaLDAP(user, login, password, source)
+	case models.LoginSMTP:
+		user, err = models.LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*models.SMTPConfig))
+	case models.LoginPAM:
+		user, err = models.LoginViaPAM(user, login, password, source.ID, source.Cfg.(*models.PAMConfig))
+	default:
+		return nil, models.ErrUnsupportedLoginType
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
+	// user could be hint to resend confirm email.
+	if user.ProhibitLogin {
+		return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
+	}
+
+	return user, nil
+}

From 331797e081300bb45dd4fc3321d61112b248df8f Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Fri, 18 Jun 2021 23:04:32 +0100
Subject: [PATCH 03/44] Move Login functions out of models

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go              | 275 +---------------------------
 models/user.go                      |  16 +-
 models/user_test.go                 |   2 +-
 routers/web/admin/auths.go          |   9 +-
 routers/web/user/auth.go            |   5 +-
 routers/web/user/auth_openid.go     |   3 +-
 routers/web/user/setting/account.go |   3 +-
 services/auth/ldap/login.go         | 100 ++++++++++
 services/auth/pam/login.go          |  69 +++++++
 services/auth/signin.go             |   9 +-
 services/auth/smtp/auth.go          |  81 ++++++++
 services/auth/smtp/login.go         |  78 ++++++++
 12 files changed, 357 insertions(+), 293 deletions(-)
 create mode 100644 services/auth/ldap/login.go
 create mode 100644 services/auth/pam/login.go
 create mode 100644 services/auth/smtp/auth.go
 create mode 100644 services/auth/smtp/login.go

diff --git a/models/login_source.go b/models/login_source.go
index 42bc0f72b194c..ab319f334c7f7 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -6,23 +6,15 @@
 package models
 
 import (
-	"crypto/tls"
-	"errors"
 	"fmt"
-	"net/smtp"
-	"net/textproto"
 	"strconv"
-	"strings"
 
 	"code.gitea.io/gitea/modules/auth/ldap"
 	"code.gitea.io/gitea/modules/auth/oauth2"
-	"code.gitea.io/gitea/modules/auth/pam"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	"code.gitea.io/gitea/modules/util"
-	gouuid "github.com/google/uuid"
 	jsoniter "github.com/json-iterator/go"
 
 	"xorm.io/xorm"
@@ -472,14 +464,8 @@ func CountLoginSources() int64 {
 	return count
 }
 
-// .____     ________      _____ __________
-// |    |    \______ \    /  _  \\______   \
-// |    |     |    |  \  /  /_\  \|     ___/
-// |    |___  |    `   \/    |    \    |
-// |_______ \/_______  /\____|__  /____|
-//         \/        \/         \/
-
-func composeFullName(firstname, surname, username string) string {
+// ComposeFullName composes a firstname surname or username
+func ComposeFullName(firstname, surname, username string) string {
 	switch {
 	case len(firstname) == 0 && len(surname) == 0:
 		return username
@@ -491,260 +477,3 @@ func composeFullName(firstname, surname, username string) string {
 		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(user *User, login, password string, source *LoginSource) (*User, error) {
-	sr := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LoginDLDAP)
-	if sr == nil {
-		// User not in LDAP, do nothing
-		return nil, ErrUserNotExist{0, login, 0}
-	}
-
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.LDAP().AttributeSSHPublicKey)) > 0
-
-	// Update User admin flag if exist
-	if isExist, err := IsUserExist(0, sr.Username); err != nil {
-		return nil, err
-	} else if isExist {
-		if user == nil {
-			user, err = GetUserByName(sr.Username)
-			if err != nil {
-				return nil, err
-			}
-		}
-		if user != nil && !user.ProhibitLogin {
-			cols := make([]string, 0)
-			if len(source.LDAP().AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
-				// Change existing admin flag only if AdminFilter option is set
-				user.IsAdmin = sr.IsAdmin
-				cols = append(cols, "is_admin")
-			}
-			if !user.IsAdmin && len(source.LDAP().RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
-				// Change existing restricted flag only if RestrictedFilter option is set
-				user.IsRestricted = sr.IsRestricted
-				cols = append(cols, "is_restricted")
-			}
-			if len(cols) > 0 {
-				err = UpdateUserCols(user, cols...)
-				if err != nil {
-					return nil, err
-				}
-			}
-		}
-	}
-
-	if user != nil {
-		if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
-			return user, RewriteAllPublicKeys()
-		}
-
-		return user, nil
-	}
-
-	// Fallback.
-	if len(sr.Username) == 0 {
-		sr.Username = login
-	}
-
-	if len(sr.Mail) == 0 {
-		sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
-	}
-
-	user = &User{
-		LowerName:    strings.ToLower(sr.Username),
-		Name:         sr.Username,
-		FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
-		Email:        sr.Mail,
-		LoginType:    source.Type,
-		LoginSource:  source.ID,
-		LoginName:    login,
-		IsActive:     true,
-		IsAdmin:      sr.IsAdmin,
-		IsRestricted: sr.IsRestricted,
-	}
-
-	err := CreateUser(user)
-
-	if err == nil && isAttributeSSHPublicKeySet && addLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
-		err = RewriteAllPublicKeys()
-	}
-
-	return user, err
-}
-
-//   _________   __________________________
-//  /   _____/  /     \__    ___/\______   \
-//  \_____  \  /  \ /  \|    |    |     ___/
-//  /        \/    Y    \    |    |    |
-// /_______  /\____|__  /____|    |____|
-//         \/         \/
-
-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
-}
-
-// SMTP authentication type names.
-const (
-	SMTPPlain = "PLAIN"
-	SMTPLogin = "LOGIN"
-)
-
-// SMTPAuths contains available SMTP authentication type names.
-var SMTPAuths = []string{SMTPPlain, SMTPLogin}
-
-// SMTPAuth performs an SMTP authentication.
-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 {
-		return c.Auth(a)
-	}
-	return ErrUnsupportedLoginType
-}
-
-// LoginViaSMTP queries if login/password is valid against the SMTP,
-// and create a local user if success when enabled.
-func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig) (*User, error) {
-	// Verify allowed domains.
-	if len(cfg.AllowedDomains) > 0 {
-		idx := strings.Index(login, "@")
-		if idx == -1 {
-			return nil, ErrUserNotExist{0, login, 0}
-		} else if !util.IsStringInSlice(login[idx+1:], strings.Split(cfg.AllowedDomains, ","), true) {
-			return nil, ErrUserNotExist{0, login, 0}
-		}
-	}
-
-	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 auth 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{0, login, 0}
-		}
-		return nil, err
-	}
-
-	if user != nil {
-		return user, 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,
-		LoginType:   LoginSMTP,
-		LoginSource: sourceID,
-		LoginName:   login,
-		IsActive:    true,
-	}
-	return user, CreateUser(user)
-}
-
-// __________  _____      _____
-// \______   \/  _  \    /     \
-//  |     ___/  /_\  \  /  \ /  \
-//  |    |  /    |    \/    Y    \
-//  |____|  \____|__  /\____|__  /
-//                  \/         \/
-
-// LoginViaPAM queries if login/password is valid against the PAM,
-// and create a local user if success when enabled.
-func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) {
-	pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
-	if err != nil {
-		if strings.Contains(err.Error(), "Authentication failure") {
-			return nil, ErrUserNotExist{0, login, 0}
-		}
-		return nil, err
-	}
-
-	if user != nil {
-		return user, nil
-	}
-
-	// Allow PAM sources with `@` in their name, like from Active Directory
-	username := pamLogin
-	email := pamLogin
-	idx := strings.Index(pamLogin, "@")
-	if idx > -1 {
-		username = pamLogin[:idx]
-	}
-	if ValidateEmail(email) != nil {
-		if cfg.EmailDomain != "" {
-			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
-		} else {
-			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
-		}
-		if ValidateEmail(email) != nil {
-			email = gouuid.New().String() + "@localhost"
-		}
-	}
-
-	user = &User{
-		LowerName:   strings.ToLower(username),
-		Name:        username,
-		Email:       email,
-		Passwd:      password,
-		LoginType:   LoginPAM,
-		LoginSource: sourceID,
-		LoginName:   login, // This is what the user typed in
-		IsActive:    true,
-	}
-	return user, CreateUser(user)
-}
diff --git a/models/user.go b/models/user.go
index 5998341422197..9be5324d3042a 100644
--- a/models/user.go
+++ b/models/user.go
@@ -1658,8 +1658,8 @@ func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
 	return sshKeysNeedUpdate, nil
 }
 
-// addLdapSSHPublicKeys add a users public keys. Returns true if there are changes.
-func addLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+// AddLdapSSHPublicKeys add a users public keys. Returns true if there are changes.
+func AddLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 	var sshKeysNeedUpdate bool
 	for _, sshKey := range sshPublicKeys {
 		var err error
@@ -1696,8 +1696,8 @@ func addLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) boo
 	return sshKeysNeedUpdate
 }
 
-// synchronizeLdapSSHPublicKeys updates a users public keys. Returns true if there are changes.
-func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+// SynchronizeLdapSSHPublicKeys updates a users public keys. Returns true if there are changes.
+func SynchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 	var sshKeysNeedUpdate bool
 
 	log.Trace("synchronizeLdapSSHPublicKeys[%s]: Handling LDAP Public SSH Key synchronization for user %s", s.Name, usr.Name)
@@ -1739,7 +1739,7 @@ func synchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []str
 			newLdapSSHKeys = append(newLdapSSHKeys, LDAPPublicSSHKey)
 		}
 	}
-	if addLdapSSHPublicKeys(usr, s, newLdapSSHKeys) {
+	if AddLdapSSHPublicKeys(usr, s, newLdapSSHKeys) {
 		sshKeysNeedUpdate = true
 	}
 
@@ -1854,7 +1854,7 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 					}
 				}
 
-				fullName := composeFullName(su.Name, su.Surname, su.Username)
+				fullName := ComposeFullName(su.Name, su.Surname, su.Username)
 				// If no existing user found, create one
 				if usr == nil {
 					log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
@@ -1878,7 +1878,7 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 						log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
 					} else if isAttributeSSHPublicKeySet {
 						log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name)
-						if addLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
+						if AddLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
 							sshKeysNeedUpdate = true
 						}
 					}
@@ -1886,7 +1886,7 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 					existingUsers = append(existingUsers, usr.ID)
 
 					// Synchronize SSH Public Key if that attribute is set
-					if isAttributeSSHPublicKeySet && synchronizeLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
+					if isAttributeSSHPublicKeySet && SynchronizeLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
 						sshKeysNeedUpdate = true
 					}
 
diff --git a/models/user_test.go b/models/user_test.go
index 39a1b3c989c05..8d95233c2e63f 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -451,7 +451,7 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib
 
 	for i, kase := range testCases {
 		s.ID = int64(i) + 20
-		addLdapSSHPublicKeys(user, s, []string{kase.keyString})
+		AddLdapSSHPublicKeys(user, s, []string{kase.keyString})
 		keys, err := ListPublicLdapSSHKeys(user.ID, s.ID)
 		assert.NoError(t, err)
 		if err != nil {
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index a2f9ab0a5c326..7e316a96a7d79 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/auth/smtp"
 	"code.gitea.io/gitea/services/forms"
 
 	"xorm.io/xorm/convert"
@@ -94,7 +95,7 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.Data["is_sync_enabled"] = true
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 
@@ -218,7 +219,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 	ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 
@@ -297,7 +298,7 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["PageIsAdminAuthentications"] = true
 
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 
@@ -322,7 +323,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminAuthentications"] = true
 
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
 	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
 	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
 
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 827b7cdef0651..9309a111cd463 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -27,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -174,7 +175,7 @@ func SignInPost(ctx *context.Context) {
 	}
 
 	form := web.GetForm(ctx).(*forms.SignInForm)
-	u, err := models.UserSignIn(form.UserName, form.Password)
+	u, err := auth.UserSignIn(form.UserName, form.Password)
 	if err != nil {
 		if models.IsErrUserNotExist(err) {
 			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
@@ -901,7 +902,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 		return
 	}
 
-	u, err := models.UserSignIn(signInForm.UserName, signInForm.Password)
+	u, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
 	if err != nil {
 		if models.IsErrUserNotExist(err) {
 			ctx.Data["user_exists"] = true
diff --git a/routers/web/user/auth_openid.go b/routers/web/user/auth_openid.go
index 1a73a08c4862d..3e3da71ac538b 100644
--- a/routers/web/user/auth_openid.go
+++ b/routers/web/user/auth_openid.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -290,7 +291,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
 	ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
 	ctx.Data["OpenID"] = oid
 
-	u, err := models.UserSignIn(form.UserName, form.Password)
+	u, err := auth.UserSignIn(form.UserName, form.Password)
 	if err != nil {
 		if models.IsErrUserNotExist(err) {
 			ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 48ab37d9369e6..66688dbc730e6 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 )
@@ -227,7 +228,7 @@ func DeleteAccount(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsSettingsAccount"] = true
 
-	if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
+	if _, err := auth.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil {
 		if models.IsErrUserNotExist(err) {
 			loadAccountData(ctx)
 
diff --git a/services/auth/ldap/login.go b/services/auth/ldap/login.go
new file mode 100644
index 0000000000000..78c2ec18a2356
--- /dev/null
+++ b/services/auth/ldap/login.go
@@ -0,0 +1,100 @@
+// Copyright 2021 The Gitea 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"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+)
+
+// .____     ________      _____ __________
+// |    |    \______ \    /  _  \\______   \
+// |    |     |    |  \  /  /_\  \|     ___/
+// |    |___  |    `   \/    |    \    |
+// |_______ \/_______  /\____|__  /____|
+//         \/        \/         \/
+
+// Login queries if login/password is valid against the LDAP directory pool,
+// and create a local user if success when enabled.
+func Login(user *models.User, login, password string, source *models.LoginSource) (*models.User, error) {
+	sr := source.Cfg.(*models.LDAPConfig).SearchEntry(login, password, source.Type == models.LoginDLDAP)
+	if sr == nil {
+		// User not in LDAP, do nothing
+		return nil, models.ErrUserNotExist{Name: login}
+	}
+
+	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.LDAP().AttributeSSHPublicKey)) > 0
+
+	// Update User admin flag if exist
+	if isExist, err := models.IsUserExist(0, sr.Username); err != nil {
+		return nil, err
+	} else if isExist {
+		if user == nil {
+			user, err = models.GetUserByName(sr.Username)
+			if err != nil {
+				return nil, err
+			}
+		}
+		if user != nil && !user.ProhibitLogin {
+			cols := make([]string, 0)
+			if len(source.LDAP().AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
+				// Change existing admin flag only if AdminFilter option is set
+				user.IsAdmin = sr.IsAdmin
+				cols = append(cols, "is_admin")
+			}
+			if !user.IsAdmin && len(source.LDAP().RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
+				// Change existing restricted flag only if RestrictedFilter option is set
+				user.IsRestricted = sr.IsRestricted
+				cols = append(cols, "is_restricted")
+			}
+			if len(cols) > 0 {
+				err = models.UpdateUserCols(user, cols...)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+	}
+
+	if user != nil {
+		if isAttributeSSHPublicKeySet && models.SynchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
+			return user, models.RewriteAllPublicKeys()
+		}
+
+		return user, nil
+	}
+
+	// Fallback.
+	if len(sr.Username) == 0 {
+		sr.Username = login
+	}
+
+	if len(sr.Mail) == 0 {
+		sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
+	}
+
+	user = &models.User{
+		LowerName:    strings.ToLower(sr.Username),
+		Name:         sr.Username,
+		FullName:     models.ComposeFullName(sr.Name, sr.Surname, sr.Username),
+		Email:        sr.Mail,
+		LoginType:    source.Type,
+		LoginSource:  source.ID,
+		LoginName:    login,
+		IsActive:     true,
+		IsAdmin:      sr.IsAdmin,
+		IsRestricted: sr.IsRestricted,
+	}
+
+	err := models.CreateUser(user)
+
+	if err == nil && isAttributeSSHPublicKeySet && models.AddLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
+		err = models.RewriteAllPublicKeys()
+	}
+
+	return user, err
+}
diff --git a/services/auth/pam/login.go b/services/auth/pam/login.go
new file mode 100644
index 0000000000000..601a724be9565
--- /dev/null
+++ b/services/auth/pam/login.go
@@ -0,0 +1,69 @@
+// Copyright 2021 The Gitea 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 (
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/pam"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/google/uuid"
+)
+
+// __________  _____      _____
+// \______   \/  _  \    /     \
+//  |     ___/  /_\  \  /  \ /  \
+//  |    |  /    |    \/    Y    \
+//  |____|  \____|__  /\____|__  /
+//                  \/         \/
+
+// Login queries if login/password is valid against the PAM,
+// and create a local user if success when enabled.
+func Login(user *models.User, login, password string, sourceID int64, cfg *models.PAMConfig) (*models.User, error) {
+	pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
+	if err != nil {
+		if strings.Contains(err.Error(), "Authentication failure") {
+			return nil, models.ErrUserNotExist{Name: login}
+		}
+		return nil, err
+	}
+
+	if user != nil {
+		return user, nil
+	}
+
+	// Allow PAM sources with `@` in their name, like from Active Directory
+	username := pamLogin
+	email := pamLogin
+	idx := strings.Index(pamLogin, "@")
+	if idx > -1 {
+		username = pamLogin[:idx]
+	}
+	if models.ValidateEmail(email) != nil {
+		if cfg.EmailDomain != "" {
+			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
+		} else {
+			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
+		}
+		if models.ValidateEmail(email) != nil {
+			email = uuid.New().String() + "@localhost"
+		}
+	}
+
+	user = &models.User{
+		LowerName:   strings.ToLower(username),
+		Name:        username,
+		Email:       email,
+		Passwd:      password,
+		LoginType:   models.LoginPAM,
+		LoginSource: sourceID,
+		LoginName:   login, // This is what the user typed in
+		IsActive:    true,
+	}
+	return user, models.CreateUser(user)
+}
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 8a0c16eddfad8..04abed4b8b568 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/auth/ldap"
+	"code.gitea.io/gitea/services/auth/pam"
+	"code.gitea.io/gitea/services/auth/smtp"
 )
 
 // UserSignIn validates user name and password.
@@ -110,11 +113,11 @@ func ExternalUserLogin(user *models.User, login, password string, source *models
 	var err error
 	switch source.Type {
 	case models.LoginLDAP, models.LoginDLDAP:
-		user, err = models.LoginViaLDAP(user, login, password, source)
+		user, err = ldap.Login(user, login, password, source)
 	case models.LoginSMTP:
-		user, err = models.LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*models.SMTPConfig))
+		user, err = smtp.Login(user, login, password, source.ID, source.Cfg.(*models.SMTPConfig))
 	case models.LoginPAM:
-		user, err = models.LoginViaPAM(user, login, password, source.ID, source.Cfg.(*models.PAMConfig))
+		user, err = pam.Login(user, login, password, source.ID, source.Cfg.(*models.PAMConfig))
 	default:
 		return nil, models.ErrUnsupportedLoginType
 	}
diff --git a/services/auth/smtp/auth.go b/services/auth/smtp/auth.go
new file mode 100644
index 0000000000000..a4a7c8ed38a08
--- /dev/null
+++ b/services/auth/smtp/auth.go
@@ -0,0 +1,81 @@
+// Copyright 2021 The Gitea 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"
+	"errors"
+	"fmt"
+	"net/smtp"
+
+	"code.gitea.io/gitea/models"
+)
+
+//   _________   __________________________
+//  /   _____/  /     \__    ___/\______   \
+//  \_____  \  /  \ /  \|    |    |     ___/
+//  /        \/    Y    \    |    |    |
+// /_______  /\____|__  /____|    |____|
+//         \/         \/
+
+type loginAuthenticator struct {
+	username, password string
+}
+
+func (auth *loginAuthenticator) Start(server *smtp.ServerInfo) (string, []byte, error) {
+	return "LOGIN", []byte(auth.username), nil
+}
+
+func (auth *loginAuthenticator) Next(fromServer []byte, more bool) ([]byte, error) {
+	if more {
+		switch string(fromServer) {
+		case "Username:":
+			return []byte(auth.username), nil
+		case "Password:":
+			return []byte(auth.password), nil
+		}
+	}
+	return nil, nil
+}
+
+// SMTP authentication type names.
+const (
+	PlainAuthentication = "PLAIN"
+	LoginAuthentication = "LOGIN"
+)
+
+// Authenticators contains available SMTP authentication type names.
+var Authenticators = []string{PlainAuthentication, LoginAuthentication}
+
+// Authenticate performs an SMTP authentication.
+func Authenticate(a smtp.Auth, cfg *models.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 {
+		return c.Auth(a)
+	}
+	return models.ErrUnsupportedLoginType
+}
diff --git a/services/auth/smtp/login.go b/services/auth/smtp/login.go
new file mode 100644
index 0000000000000..3248913deb90d
--- /dev/null
+++ b/services/auth/smtp/login.go
@@ -0,0 +1,78 @@
+// Copyright 2021 The Gitea 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 (
+	"errors"
+	"net/smtp"
+	"net/textproto"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/util"
+)
+
+//   _________   __________________________
+//  /   _____/  /     \__    ___/\______   \
+//  \_____  \  /  \ /  \|    |    |     ___/
+//  /        \/    Y    \    |    |    |
+// /_______  /\____|__  /____|    |____|
+//         \/         \/
+
+// Login queries if login/password is valid against the SMTP,
+// and create a local user if success when enabled.
+func Login(user *models.User, login, password string, sourceID int64, cfg *models.SMTPConfig) (*models.User, error) {
+	// Verify allowed domains.
+	if len(cfg.AllowedDomains) > 0 {
+		idx := strings.Index(login, "@")
+		if idx == -1 {
+			return nil, models.ErrUserNotExist{Name: login}
+		} else if !util.IsStringInSlice(login[idx+1:], strings.Split(cfg.AllowedDomains, ","), true) {
+			return nil, models.ErrUserNotExist{Name: login}
+		}
+	}
+
+	var auth smtp.Auth
+	if cfg.Auth == PlainAuthentication {
+		auth = smtp.PlainAuth("", login, password, cfg.Host)
+	} else if cfg.Auth == LoginAuthentication {
+		auth = &loginAuthenticator{login, password}
+	} else {
+		return nil, errors.New("Unsupported SMTP auth type")
+	}
+
+	if err := Authenticate(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, models.ErrUserNotExist{Name: login}
+		}
+		return nil, err
+	}
+
+	if user != nil {
+		return user, nil
+	}
+
+	username := login
+	idx := strings.Index(login, "@")
+	if idx > -1 {
+		username = login[:idx]
+	}
+
+	user = &models.User{
+		LowerName:   strings.ToLower(username),
+		Name:        strings.ToLower(username),
+		Email:       login,
+		Passwd:      password,
+		LoginType:   models.LoginSMTP,
+		LoginSource: sourceID,
+		LoginName:   login,
+		IsActive:    true,
+	}
+	return user, models.CreateUser(user)
+}

From d1d9e44974c6f0d91444db7af94220c21541e670 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 19 Jun 2021 08:22:41 +0100
Subject: [PATCH 04/44] actually lets make these sources

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/signin.go                  | 6 +++---
 services/auth/{ => source}/ldap/login.go | 0
 services/auth/{ => source}/pam/login.go  | 0
 services/auth/{ => source}/smtp/auth.go  | 0
 services/auth/{ => source}/smtp/login.go | 0
 5 files changed, 3 insertions(+), 3 deletions(-)
 rename services/auth/{ => source}/ldap/login.go (100%)
 rename services/auth/{ => source}/pam/login.go (100%)
 rename services/auth/{ => source}/smtp/auth.go (100%)
 rename services/auth/{ => source}/smtp/login.go (100%)

diff --git a/services/auth/signin.go b/services/auth/signin.go
index 04abed4b8b568..b49761e3a60aa 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -10,9 +10,9 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/services/auth/ldap"
-	"code.gitea.io/gitea/services/auth/pam"
-	"code.gitea.io/gitea/services/auth/smtp"
+	"code.gitea.io/gitea/services/auth/source/ldap"
+	"code.gitea.io/gitea/services/auth/source/pam"
+	"code.gitea.io/gitea/services/auth/source/smtp"
 )
 
 // UserSignIn validates user name and password.
diff --git a/services/auth/ldap/login.go b/services/auth/source/ldap/login.go
similarity index 100%
rename from services/auth/ldap/login.go
rename to services/auth/source/ldap/login.go
diff --git a/services/auth/pam/login.go b/services/auth/source/pam/login.go
similarity index 100%
rename from services/auth/pam/login.go
rename to services/auth/source/pam/login.go
diff --git a/services/auth/smtp/auth.go b/services/auth/source/smtp/auth.go
similarity index 100%
rename from services/auth/smtp/auth.go
rename to services/auth/source/smtp/auth.go
diff --git a/services/auth/smtp/login.go b/services/auth/source/smtp/login.go
similarity index 100%
rename from services/auth/smtp/login.go
rename to services/auth/source/smtp/login.go

From d98bbbcd8531f42c6c109ca02abb0bb018aa2568 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 19 Jun 2021 13:04:31 +0100
Subject: [PATCH 05/44] Move SyncExternal to services/auth

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 integrations/auth_ldap_test.go     |   6 +-
 models/login_source.go             |  14 --
 models/ssh_key.go                  |   4 +-
 models/user.go                     | 263 ++++-------------------------
 models/user_test.go                |   4 +-
 modules/cron/tasks_basic.go        |   3 +-
 routers/web/admin/auths.go         |   2 +-
 services/auth/source/ldap/login.go |   6 +-
 services/auth/source/ldap/sync.go  | 190 +++++++++++++++++++++
 services/auth/source/ldap/util.go  |  19 +++
 services/auth/sync.go              |  44 +++++
 11 files changed, 303 insertions(+), 252 deletions(-)
 create mode 100644 services/auth/source/ldap/sync.go
 create mode 100644 services/auth/source/ldap/util.go
 create mode 100644 services/auth/sync.go

diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go
index 4d82c092e7280..e714dbd1326c9 100644
--- a/integrations/auth_ldap_test.go
+++ b/integrations/auth_ldap_test.go
@@ -11,7 +11,7 @@ import (
 	"strings"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/unknwon/i18n"
@@ -151,7 +151,7 @@ func TestLDAPUserSync(t *testing.T) {
 	}
 	defer prepareTestEnv(t)()
 	addAuthSourceLDAP(t, "")
-	models.SyncExternalUsers(context.Background(), true)
+	auth.SyncExternalUsers(context.Background(), true)
 
 	session := loginUser(t, "user1")
 	// Check if users exists
@@ -216,7 +216,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
 	defer prepareTestEnv(t)()
 	addAuthSourceLDAP(t, "sshPublicKey")
 
-	models.SyncExternalUsers(context.Background(), true)
+	auth.SyncExternalUsers(context.Background(), true)
 
 	// Check if users has SSH keys synced
 	for _, u := range gitLDAPUsers {
diff --git a/models/login_source.go b/models/login_source.go
index ab319f334c7f7..0ac24d680a571 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -463,17 +463,3 @@ func CountLoginSources() int64 {
 	count, _ := x.Count(new(LoginSource))
 	return count
 }
-
-// ComposeFullName composes a firstname surname or username
-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
-	}
-}
diff --git a/models/ssh_key.go b/models/ssh_key.go
index e35fc12e080a2..a51ee2f7a4a3d 100644
--- a/models/ssh_key.go
+++ b/models/ssh_key.go
@@ -635,8 +635,8 @@ func ListPublicKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
 	return keys, sess.Find(&keys)
 }
 
-// ListPublicLdapSSHKeys returns a list of synchronized public ldap ssh keys belongs to given user and login source.
-func ListPublicLdapSSHKeys(uid, loginSourceID int64) ([]*PublicKey, error) {
+// ListPublicKeysBySource returns a list of synchronized public keys for a given user and login source.
+func ListPublicKeysBySource(uid, loginSourceID int64) ([]*PublicKey, error) {
 	keys := make([]*PublicKey, 0, 5)
 	return keys, x.
 		Where("owner_id = ? AND login_source_id = ?", uid, loginSourceID).
diff --git a/models/user.go b/models/user.go
index 9be5324d3042a..e5d8f5fdaba19 100644
--- a/models/user.go
+++ b/models/user.go
@@ -1397,6 +1397,13 @@ func GetUserIDsByNames(names []string, ignoreNonExistent bool) ([]int64, error)
 	return ids, nil
 }
 
+// GetUsersBySource returns a list of Users for a login source
+func GetUsersBySource(s *LoginSource) ([]*User, error) {
+	var users []*User
+	err := x.Where("login_type = ? AND login_source = ?", s.Type, s.ID).Find(&users)
+	return users, err
+}
+
 // UserCommit represents a commit with validation of user.
 type UserCommit struct {
 	User *User
@@ -1658,8 +1665,8 @@ func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
 	return sshKeysNeedUpdate, nil
 }
 
-// AddLdapSSHPublicKeys add a users public keys. Returns true if there are changes.
-func AddLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+// AddPublicKeysBySource add a users public keys. Returns true if there are changes.
+func AddPublicKeysBySource(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 	var sshKeysNeedUpdate bool
 	for _, sshKey := range sshPublicKeys {
 		var err error
@@ -1680,82 +1687,82 @@ func AddLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) boo
 
 			if _, err := AddPublicKey(usr.ID, sshKeyName, marshalled, s.ID); err != nil {
 				if IsErrKeyAlreadyExist(err) {
-					log.Trace("addLdapSSHPublicKeys[%s]: LDAP Public SSH Key %s already exists for user", sshKeyName, usr.Name)
+					log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
 				} else {
-					log.Error("addLdapSSHPublicKeys[%s]: Error adding LDAP Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
+					log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
 				}
 			} else {
-				log.Trace("addLdapSSHPublicKeys[%s]: Added LDAP Public SSH Key for user %s", sshKeyName, usr.Name)
+				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
 				sshKeysNeedUpdate = true
 			}
 		}
 		if !found && err != nil {
-			log.Warn("addLdapSSHPublicKeys[%s]: Skipping invalid LDAP Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
+			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
 		}
 	}
 	return sshKeysNeedUpdate
 }
 
-// SynchronizeLdapSSHPublicKeys updates a users public keys. Returns true if there are changes.
-func SynchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
+func SynchronizePublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
 	var sshKeysNeedUpdate bool
 
-	log.Trace("synchronizeLdapSSHPublicKeys[%s]: Handling LDAP Public SSH Key synchronization for user %s", s.Name, usr.Name)
+	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
 
 	// Get Public Keys from DB with current LDAP source
 	var giteaKeys []string
-	keys, err := ListPublicLdapSSHKeys(usr.ID, s.ID)
+	keys, err := ListPublicKeysBySource(usr.ID, s.ID)
 	if err != nil {
-		log.Error("synchronizeLdapSSHPublicKeys[%s]: Error listing LDAP Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
+		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
 	}
 
 	for _, v := range keys {
 		giteaKeys = append(giteaKeys, v.OmitEmail())
 	}
 
-	// Get Public Keys from LDAP and skip duplicate keys
-	var ldapKeys []string
+	// Process the provided keys to remove duplicates and name part
+	var providedKeys []string
 	for _, v := range sshPublicKeys {
 		sshKeySplit := strings.Split(v, " ")
 		if len(sshKeySplit) > 1 {
-			ldapKey := strings.Join(sshKeySplit[:2], " ")
-			if !util.ExistsInSlice(ldapKey, ldapKeys) {
-				ldapKeys = append(ldapKeys, ldapKey)
+			key := strings.Join(sshKeySplit[:2], " ")
+			if !util.ExistsInSlice(key, providedKeys) {
+				providedKeys = append(providedKeys, key)
 			}
 		}
 	}
 
 	// Check if Public Key sync is needed
-	if util.IsEqualSlice(giteaKeys, ldapKeys) {
-		log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Keys are already in sync for %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys))
+	if util.IsEqualSlice(giteaKeys, providedKeys) {
+		log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
 		return false
 	}
-	log.Trace("synchronizeLdapSSHPublicKeys[%s]: LDAP Public Key needs update for user %s (LDAP:%v/DB:%v)", s.Name, usr.Name, len(ldapKeys), len(giteaKeys))
+	log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
 
-	// Add LDAP Public SSH Keys that doesn't already exist in DB
-	var newLdapSSHKeys []string
-	for _, LDAPPublicSSHKey := range ldapKeys {
-		if !util.ExistsInSlice(LDAPPublicSSHKey, giteaKeys) {
-			newLdapSSHKeys = append(newLdapSSHKeys, LDAPPublicSSHKey)
+	// Add new Public SSH Keys that doesn't already exist in DB
+	var newKeys []string
+	for _, key := range providedKeys {
+		if !util.ExistsInSlice(key, giteaKeys) {
+			newKeys = append(newKeys, key)
 		}
 	}
-	if AddLdapSSHPublicKeys(usr, s, newLdapSSHKeys) {
+	if AddPublicKeysBySource(usr, s, newKeys) {
 		sshKeysNeedUpdate = true
 	}
 
-	// Mark LDAP keys from DB that doesn't exist in LDAP for deletion
+	// Mark keys from DB that no longer exist in the source for deletion
 	var giteaKeysToDelete []string
 	for _, giteaKey := range giteaKeys {
-		if !util.ExistsInSlice(giteaKey, ldapKeys) {
-			log.Trace("synchronizeLdapSSHPublicKeys[%s]: Marking LDAP Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
+		if !util.ExistsInSlice(giteaKey, providedKeys) {
+			log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
 			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
 		}
 	}
 
-	// Delete LDAP keys from DB that doesn't exist in LDAP
+	// Delete keys from DB that no longer exist in the source
 	needUpd, err := deleteKeysMarkedForDeletion(giteaKeysToDelete)
 	if err != nil {
-		log.Error("synchronizeLdapSSHPublicKeys[%s]: Error deleting LDAP Public SSH Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
+		log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
 	}
 	if needUpd {
 		sshKeysNeedUpdate = true
@@ -1764,202 +1771,6 @@ func SynchronizeLdapSSHPublicKeys(usr *User, s *LoginSource, sshPublicKeys []str
 	return sshKeysNeedUpdate
 }
 
-// SyncExternalUsers is used to synchronize users with external authorization source
-func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
-	log.Trace("Doing: SyncExternalUsers")
-
-	ls, err := LoginSources()
-	if err != nil {
-		log.Error("SyncExternalUsers: %v", err)
-		return err
-	}
-
-	for _, s := range ls {
-		if !s.IsActived || !s.IsSyncEnabled {
-			continue
-		}
-		select {
-		case <-ctx.Done():
-			log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
-			return ErrCancelledf("Before update of %s", s.Name)
-		default:
-		}
-
-		if s.IsLDAP() {
-			log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
-
-			var existingUsers []int64
-			isAttributeSSHPublicKeySet := len(strings.TrimSpace(s.LDAP().AttributeSSHPublicKey)) > 0
-			var sshKeysNeedUpdate bool
-
-			// Find all users with this login type
-			var users []*User
-			err = x.Where("login_type = ?", LoginLDAP).
-				And("login_source = ?", s.ID).
-				Find(&users)
-			if err != nil {
-				log.Error("SyncExternalUsers: %v", err)
-				return err
-			}
-			select {
-			case <-ctx.Done():
-				log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
-				return ErrCancelledf("Before update of %s", s.Name)
-			default:
-			}
-
-			sr, err := s.LDAP().SearchEntries()
-			if err != nil {
-				log.Error("SyncExternalUsers LDAP source failure [%s], skipped", s.Name)
-				continue
-			}
-
-			if len(sr) == 0 {
-				if !s.LDAP().AllowDeactivateAll {
-					log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
-					continue
-				} else {
-					log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
-				}
-			}
-
-			for _, su := range sr {
-				select {
-				case <-ctx.Done():
-					log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name)
-					// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
-					if sshKeysNeedUpdate {
-						err = RewriteAllPublicKeys()
-						if err != nil {
-							log.Error("RewriteAllPublicKeys: %v", err)
-						}
-					}
-					return ErrCancelledf("During update of %s before completed update of users", s.Name)
-				default:
-				}
-				if len(su.Username) == 0 {
-					continue
-				}
-
-				if len(su.Mail) == 0 {
-					su.Mail = fmt.Sprintf("%s@localhost", su.Username)
-				}
-
-				var usr *User
-				// Search for existing user
-				for _, du := range users {
-					if du.LowerName == strings.ToLower(su.Username) {
-						usr = du
-						break
-					}
-				}
-
-				fullName := ComposeFullName(su.Name, su.Surname, su.Username)
-				// If no existing user found, create one
-				if usr == nil {
-					log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
-
-					usr = &User{
-						LowerName:    strings.ToLower(su.Username),
-						Name:         su.Username,
-						FullName:     fullName,
-						LoginType:    s.Type,
-						LoginSource:  s.ID,
-						LoginName:    su.Username,
-						Email:        su.Mail,
-						IsAdmin:      su.IsAdmin,
-						IsRestricted: su.IsRestricted,
-						IsActive:     true,
-					}
-
-					err = CreateUser(usr)
-
-					if err != nil {
-						log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
-					} else if isAttributeSSHPublicKeySet {
-						log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name)
-						if AddLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
-							sshKeysNeedUpdate = true
-						}
-					}
-				} else if updateExisting {
-					existingUsers = append(existingUsers, usr.ID)
-
-					// Synchronize SSH Public Key if that attribute is set
-					if isAttributeSSHPublicKeySet && SynchronizeLdapSSHPublicKeys(usr, s, su.SSHPublicKey) {
-						sshKeysNeedUpdate = true
-					}
-
-					// Check if user data has changed
-					if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
-						(len(s.LDAP().RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
-						!strings.EqualFold(usr.Email, su.Mail) ||
-						usr.FullName != fullName ||
-						!usr.IsActive {
-
-						log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
-
-						usr.FullName = fullName
-						usr.Email = su.Mail
-						// Change existing admin flag only if AdminFilter option is set
-						if len(s.LDAP().AdminFilter) > 0 {
-							usr.IsAdmin = su.IsAdmin
-						}
-						// Change existing restricted flag only if RestrictedFilter option is set
-						if !usr.IsAdmin && len(s.LDAP().RestrictedFilter) > 0 {
-							usr.IsRestricted = su.IsRestricted
-						}
-						usr.IsActive = true
-
-						err = UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
-						if err != nil {
-							log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
-						}
-					}
-				}
-			}
-
-			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
-			if sshKeysNeedUpdate {
-				err = RewriteAllPublicKeys()
-				if err != nil {
-					log.Error("RewriteAllPublicKeys: %v", err)
-				}
-			}
-
-			select {
-			case <-ctx.Done():
-				log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name)
-				return ErrCancelledf("During update of %s before delete users", s.Name)
-			default:
-			}
-
-			// Deactivate users not present in LDAP
-			if updateExisting {
-				for _, usr := range users {
-					found := false
-					for _, uid := range existingUsers {
-						if usr.ID == uid {
-							found = true
-							break
-						}
-					}
-					if !found {
-						log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
-
-						usr.IsActive = false
-						err = UpdateUserCols(usr, "is_active")
-						if err != nil {
-							log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
-						}
-					}
-				}
-			}
-		}
-	}
-	return nil
-}
-
 // IterateUser iterate users
 func IterateUser(f func(user *User) error) error {
 	var start int
diff --git a/models/user_test.go b/models/user_test.go
index 8d95233c2e63f..316afe37683d7 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -451,8 +451,8 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib
 
 	for i, kase := range testCases {
 		s.ID = int64(i) + 20
-		AddLdapSSHPublicKeys(user, s, []string{kase.keyString})
-		keys, err := ListPublicLdapSSHKeys(user.ID, s.ID)
+		AddPublicKeysBySource(user, s, []string{kase.keyString})
+		keys, err := ListPublicKeysBySource(user.ID, s.ID)
 		assert.NoError(t, err)
 		if err != nil {
 			continue
diff --git a/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go
index 391cda0f891f5..a148ab06327d0 100644
--- a/modules/cron/tasks_basic.go
+++ b/modules/cron/tasks_basic.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/modules/migrations"
 	repository_service "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/auth"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 )
 
@@ -80,7 +81,7 @@ func registerSyncExternalUsers() {
 		UpdateExisting: true,
 	}, func(ctx context.Context, _ *models.User, config Config) error {
 		realConfig := config.(*UpdateExistingConfig)
-		return models.SyncExternalUsers(ctx, realConfig.UpdateExisting)
+		return auth.SyncExternalUsers(ctx, realConfig.UpdateExisting)
 	})
 }
 
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 7e316a96a7d79..2c9f215d1d8db 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -20,7 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/services/auth/smtp"
+	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/forms"
 
 	"xorm.io/xorm/convert"
diff --git a/services/auth/source/ldap/login.go b/services/auth/source/ldap/login.go
index 78c2ec18a2356..266e4f85cf155 100644
--- a/services/auth/source/ldap/login.go
+++ b/services/auth/source/ldap/login.go
@@ -61,7 +61,7 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 	}
 
 	if user != nil {
-		if isAttributeSSHPublicKeySet && models.SynchronizeLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
+		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source, sr.SSHPublicKey) {
 			return user, models.RewriteAllPublicKeys()
 		}
 
@@ -80,7 +80,7 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 	user = &models.User{
 		LowerName:    strings.ToLower(sr.Username),
 		Name:         sr.Username,
-		FullName:     models.ComposeFullName(sr.Name, sr.Surname, sr.Username),
+		FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
 		Email:        sr.Mail,
 		LoginType:    source.Type,
 		LoginSource:  source.ID,
@@ -92,7 +92,7 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 
 	err := models.CreateUser(user)
 
-	if err == nil && isAttributeSSHPublicKeySet && models.AddLdapSSHPublicKeys(user, source, sr.SSHPublicKey) {
+	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source, sr.SSHPublicKey) {
 		err = models.RewriteAllPublicKeys()
 	}
 
diff --git a/services/auth/source/ldap/sync.go b/services/auth/source/ldap/sync.go
new file mode 100644
index 0000000000000..e102a8c2deb5f
--- /dev/null
+++ b/services/auth/source/ldap/sync.go
@@ -0,0 +1,190 @@
+// Copyright 2021 The Gitea 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 (
+	"context"
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// .____     ________      _____ __________
+// |    |    \______ \    /  _  \\______   \
+// |    |     |    |  \  /  /_\  \|     ___/
+// |    |___  |    `   \/    |    \    |
+// |_______ \/_______  /\____|__  /____|
+//         \/        \/         \/
+
+func Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error {
+	log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
+
+	var existingUsers []int64
+	isAttributeSSHPublicKeySet := len(strings.TrimSpace(s.LDAP().AttributeSSHPublicKey)) > 0
+	var sshKeysNeedUpdate bool
+
+	// Find all users with this login type - FIXME: Should this be an iterator?
+	users, err := models.GetUsersBySource(s)
+	if err != nil {
+		log.Error("SyncExternalUsers: %v", err)
+		return err
+	}
+	select {
+	case <-ctx.Done():
+		log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
+		return models.ErrCancelledf("Before update of %s", s.Name)
+	default:
+	}
+
+	sr, err := s.LDAP().SearchEntries()
+	if err != nil {
+		log.Error("SyncExternalUsers LDAP source failure [%s], skipped", s.Name)
+		return nil
+	}
+
+	if len(sr) == 0 {
+		if !s.LDAP().AllowDeactivateAll {
+			log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
+			return nil
+		}
+		log.Warn("LDAP search found no entries but did not report an error. All users will be deactivated as per settings")
+	}
+
+	for _, su := range sr {
+		select {
+		case <-ctx.Done():
+			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name)
+			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
+			if sshKeysNeedUpdate {
+				err = models.RewriteAllPublicKeys()
+				if err != nil {
+					log.Error("RewriteAllPublicKeys: %v", err)
+				}
+			}
+			return models.ErrCancelledf("During update of %s before completed update of users", s.Name)
+		default:
+		}
+		if len(su.Username) == 0 {
+			continue
+		}
+
+		if len(su.Mail) == 0 {
+			su.Mail = fmt.Sprintf("%s@localhost", su.Username)
+		}
+
+		var usr *models.User
+		// Search for existing user
+		for _, du := range users {
+			if du.LowerName == strings.ToLower(su.Username) {
+				usr = du
+				break
+			}
+		}
+
+		fullName := composeFullName(su.Name, su.Surname, su.Username)
+		// If no existing user found, create one
+		if usr == nil {
+			log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
+
+			usr = &models.User{
+				LowerName:    strings.ToLower(su.Username),
+				Name:         su.Username,
+				FullName:     fullName,
+				LoginType:    s.Type,
+				LoginSource:  s.ID,
+				LoginName:    su.Username,
+				Email:        su.Mail,
+				IsAdmin:      su.IsAdmin,
+				IsRestricted: su.IsRestricted,
+				IsActive:     true,
+			}
+
+			err = models.CreateUser(usr)
+
+			if err != nil {
+				log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
+			} else if isAttributeSSHPublicKeySet {
+				log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name)
+				if models.AddPublicKeysBySource(usr, s, su.SSHPublicKey) {
+					sshKeysNeedUpdate = true
+				}
+			}
+		} else if updateExisting {
+			existingUsers = append(existingUsers, usr.ID)
+
+			// Synchronize SSH Public Key if that attribute is set
+			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, s, su.SSHPublicKey) {
+				sshKeysNeedUpdate = true
+			}
+
+			// Check if user data has changed
+			if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
+				(len(s.LDAP().RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
+				!strings.EqualFold(usr.Email, su.Mail) ||
+				usr.FullName != fullName ||
+				!usr.IsActive {
+
+				log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
+
+				usr.FullName = fullName
+				usr.Email = su.Mail
+				// Change existing admin flag only if AdminFilter option is set
+				if len(s.LDAP().AdminFilter) > 0 {
+					usr.IsAdmin = su.IsAdmin
+				}
+				// Change existing restricted flag only if RestrictedFilter option is set
+				if !usr.IsAdmin && len(s.LDAP().RestrictedFilter) > 0 {
+					usr.IsRestricted = su.IsRestricted
+				}
+				usr.IsActive = true
+
+				err = models.UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
+				if err != nil {
+					log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
+				}
+			}
+		}
+	}
+
+	// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
+	if sshKeysNeedUpdate {
+		err = models.RewriteAllPublicKeys()
+		if err != nil {
+			log.Error("RewriteAllPublicKeys: %v", err)
+		}
+	}
+
+	select {
+	case <-ctx.Done():
+		log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name)
+		return models.ErrCancelledf("During update of %s before delete users", s.Name)
+	default:
+	}
+
+	// Deactivate users not present in LDAP
+	if updateExisting {
+		for _, usr := range users {
+			found := false
+			for _, uid := range existingUsers {
+				if usr.ID == uid {
+					found = true
+					break
+				}
+			}
+			if !found {
+				log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
+
+				usr.IsActive = false
+				err = models.UpdateUserCols(usr, "is_active")
+				if err != nil {
+					log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
+				}
+			}
+		}
+	}
+	return nil
+}
diff --git a/services/auth/source/ldap/util.go b/services/auth/source/ldap/util.go
new file mode 100644
index 0000000000000..f27de37c87edb
--- /dev/null
+++ b/services/auth/source/ldap/util.go
@@ -0,0 +1,19 @@
+// Copyright 2021 The Gitea 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
+
+// composeFullName composes a firstname surname or username
+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
+	}
+}
diff --git a/services/auth/sync.go b/services/auth/sync.go
new file mode 100644
index 0000000000000..613c313f43eda
--- /dev/null
+++ b/services/auth/sync.go
@@ -0,0 +1,44 @@
+// Copyright 2021 The Gitea 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 auth
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/services/auth/source/ldap"
+)
+
+// SyncExternalUsers is used to synchronize users with external authorization source
+func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
+	log.Trace("Doing: SyncExternalUsers")
+
+	ls, err := models.LoginSources()
+	if err != nil {
+		log.Error("SyncExternalUsers: %v", err)
+		return err
+	}
+
+	for _, s := range ls {
+		if !s.IsActived || !s.IsSyncEnabled {
+			continue
+		}
+		select {
+		case <-ctx.Done():
+			log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
+			return models.ErrCancelledf("Before update of %s", s.Name)
+		default:
+		}
+
+		if s.IsLDAP() {
+			err := ldap.Sync(ctx, updateExisting, s)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}

From 1116a1fe9d7b6173245f27a5add1cfe2f5ee97ee Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 19 Jun 2021 13:15:54 +0100
Subject: [PATCH 06/44] Restructure ssh_key.go

- move functions from models/user.go that relate to ssh_key to ssh_key
- split ssh_key.go to try create clearer function domains for allow for
future refactors here.

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/ssh_key.go                       | 1090 ++---------------------
 models/ssh_key_authorized_keys.go       |  218 +++++
 models/ssh_key_authorized_principals.go |  142 +++
 models/ssh_key_deploy.go                |  299 +++++++
 models/ssh_key_fingerprint.go           |   97 ++
 models/ssh_key_parse.go                 |  309 +++++++
 models/ssh_key_principals.go            |  125 +++
 models/user.go                          |  138 ---
 8 files changed, 1288 insertions(+), 1130 deletions(-)
 create mode 100644 models/ssh_key_authorized_keys.go
 create mode 100644 models/ssh_key_authorized_principals.go
 create mode 100644 models/ssh_key_deploy.go
 create mode 100644 models/ssh_key_fingerprint.go
 create mode 100644 models/ssh_key_parse.go
 create mode 100644 models/ssh_key_principals.go

diff --git a/models/ssh_key.go b/models/ssh_key.go
index a51ee2f7a4a3d..fd8fa660683d4 100644
--- a/models/ssh_key.go
+++ b/models/ssh_key.go
@@ -6,45 +6,18 @@
 package models
 
 import (
-	"bufio"
-	"crypto/rsa"
-	"crypto/x509"
-	"encoding/asn1"
-	"encoding/base64"
-	"encoding/binary"
-	"encoding/pem"
-	"errors"
 	"fmt"
-	"io"
-	"io/ioutil"
-	"math/big"
-	"os"
-	"path/filepath"
-	"strconv"
 	"strings"
-	"sync"
 	"time"
 
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/process"
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
-
 	"golang.org/x/crypto/ssh"
-	"xorm.io/builder"
-	"xorm.io/xorm"
-)
 
-const (
-	tplCommentPrefix = `# gitea public key`
-	tplPublicKey     = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
-
-	authorizedPrincipalsFile = "authorized_principals"
+	"xorm.io/builder"
 )
 
-var sshOpLocker sync.Mutex
-
 // KeyType specifies the key type
 type KeyType int
 
@@ -86,413 +59,10 @@ func (key *PublicKey) OmitEmail() string {
 }
 
 // AuthorizedString returns formatted public key string for authorized_keys file.
+//
+// TODO: Consider dropping this function
 func (key *PublicKey) AuthorizedString() string {
-	sb := &strings.Builder{}
-	_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]interface{}{
-		"AppPath":     util.ShellEscape(setting.AppPath),
-		"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
-		"CustomConf":  util.ShellEscape(setting.CustomConf),
-		"CustomPath":  util.ShellEscape(setting.CustomPath),
-		"Key":         key,
-	})
-
-	return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
-}
-
-func extractTypeFromBase64Key(key string) (string, error) {
-	b, err := base64.StdEncoding.DecodeString(key)
-	if err != nil || len(b) < 4 {
-		return "", fmt.Errorf("invalid key format: %v", err)
-	}
-
-	keyLength := int(binary.BigEndian.Uint32(b))
-	if len(b) < 4+keyLength {
-		return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
-	}
-
-	return string(b[4 : 4+keyLength]), nil
-}
-
-const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
-
-// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
-func parseKeyString(content string) (string, error) {
-	// remove whitespace at start and end
-	content = strings.TrimSpace(content)
-
-	var keyType, keyContent, keyComment string
-
-	if strings.HasPrefix(content, ssh2keyStart) {
-		// Parse SSH2 file format.
-
-		// Transform all legal line endings to a single "\n".
-		content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
-
-		lines := strings.Split(content, "\n")
-		continuationLine := false
-
-		for _, line := range lines {
-			// Skip lines that:
-			// 1) are a continuation of the previous line,
-			// 2) contain ":" as that are comment lines
-			// 3) contain "-" as that are begin and end tags
-			if continuationLine || strings.ContainsAny(line, ":-") {
-				continuationLine = strings.HasSuffix(line, "\\")
-			} else {
-				keyContent += line
-			}
-		}
-
-		t, err := extractTypeFromBase64Key(keyContent)
-		if err != nil {
-			return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
-		}
-		keyType = t
-	} else {
-		if strings.Contains(content, "-----BEGIN") {
-			// Convert PEM Keys to OpenSSH format
-			// Transform all legal line endings to a single "\n".
-			content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
-
-			block, _ := pem.Decode([]byte(content))
-			if block == nil {
-				return "", fmt.Errorf("failed to parse PEM block containing the public key")
-			}
-
-			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
-			if err != nil {
-				var pk rsa.PublicKey
-				_, err2 := asn1.Unmarshal(block.Bytes, &pk)
-				if err2 != nil {
-					return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %v", err, err2)
-				}
-				pub = &pk
-			}
-
-			sshKey, err := ssh.NewPublicKey(pub)
-			if err != nil {
-				return "", fmt.Errorf("unable to convert to ssh public key: %v", err)
-			}
-			content = string(ssh.MarshalAuthorizedKey(sshKey))
-		}
-		// Parse OpenSSH format.
-
-		// Remove all newlines
-		content = strings.NewReplacer("\r\n", "", "\n", "").Replace(content)
-
-		parts := strings.SplitN(content, " ", 3)
-		switch len(parts) {
-		case 0:
-			return "", errors.New("empty key")
-		case 1:
-			keyContent = parts[0]
-		case 2:
-			keyType = parts[0]
-			keyContent = parts[1]
-		default:
-			keyType = parts[0]
-			keyContent = parts[1]
-			keyComment = parts[2]
-		}
-
-		// If keyType is not given, extract it from content. If given, validate it.
-		t, err := extractTypeFromBase64Key(keyContent)
-		if err != nil {
-			return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
-		}
-		if len(keyType) == 0 {
-			keyType = t
-		} else if keyType != t {
-			return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
-		}
-	}
-	// Finally we need to check whether we can actually read the proposed key:
-	_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment))
-	if err != nil {
-		return "", fmt.Errorf("invalid ssh public key: %v", err)
-	}
-	return keyType + " " + keyContent + " " + keyComment, nil
-}
-
-// writeTmpKeyFile writes key content to a temporary file
-// and returns the name of that file, along with any possible errors.
-func writeTmpKeyFile(content string) (string, error) {
-	tmpFile, err := ioutil.TempFile(setting.SSH.KeyTestPath, "gitea_keytest")
-	if err != nil {
-		return "", fmt.Errorf("TempFile: %v", err)
-	}
-	defer tmpFile.Close()
-
-	if _, err = tmpFile.WriteString(content); err != nil {
-		return "", fmt.Errorf("WriteString: %v", err)
-	}
-	return tmpFile.Name(), nil
-}
-
-// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
-func SSHKeyGenParsePublicKey(key string) (string, int, error) {
-	tmpName, err := writeTmpKeyFile(key)
-	if err != nil {
-		return "", 0, fmt.Errorf("writeTmpKeyFile: %v", err)
-	}
-	defer func() {
-		if err := util.Remove(tmpName); err != nil {
-			log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
-		}
-	}()
-
-	stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", setting.SSH.KeygenPath, "-lf", tmpName)
-	if err != nil {
-		return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
-	}
-	if strings.Contains(stdout, "is not a public key file") {
-		return "", 0, ErrKeyUnableVerify{stdout}
-	}
-
-	fields := strings.Split(stdout, " ")
-	if len(fields) < 4 {
-		return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
-	}
-
-	keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
-	length, err := strconv.ParseInt(fields[0], 10, 32)
-	if err != nil {
-		return "", 0, err
-	}
-	return strings.ToLower(keyType), int(length), nil
-}
-
-// SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
-func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
-	fields := strings.Fields(keyLine)
-	if len(fields) < 2 {
-		return "", 0, fmt.Errorf("not enough fields in public key line: %s", keyLine)
-	}
-
-	raw, err := base64.StdEncoding.DecodeString(fields[1])
-	if err != nil {
-		return "", 0, err
-	}
-
-	pkey, err := ssh.ParsePublicKey(raw)
-	if err != nil {
-		if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
-			return "", 0, ErrKeyUnableVerify{err.Error()}
-		}
-		return "", 0, fmt.Errorf("ParsePublicKey: %v", err)
-	}
-
-	// The ssh library can parse the key, so next we find out what key exactly we have.
-	switch pkey.Type() {
-	case ssh.KeyAlgoDSA:
-		rawPub := struct {
-			Name       string
-			P, Q, G, Y *big.Int
-		}{}
-		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
-			return "", 0, err
-		}
-		// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
-		// see dsa keys != 1024 bit, but as it seems to work, we will not check here
-		return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
-	case ssh.KeyAlgoRSA:
-		rawPub := struct {
-			Name string
-			E    *big.Int
-			N    *big.Int
-		}{}
-		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
-			return "", 0, err
-		}
-		return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
-	case ssh.KeyAlgoECDSA256:
-		return "ecdsa", 256, nil
-	case ssh.KeyAlgoECDSA384:
-		return "ecdsa", 384, nil
-	case ssh.KeyAlgoECDSA521:
-		return "ecdsa", 521, nil
-	case ssh.KeyAlgoED25519:
-		return "ed25519", 256, nil
-	case ssh.KeyAlgoSKECDSA256:
-		return "ecdsa-sk", 256, nil
-	case ssh.KeyAlgoSKED25519:
-		return "ed25519-sk", 256, nil
-	}
-	return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
-}
-
-// CheckPublicKeyString checks if the given public key string is recognized by SSH.
-// It returns the actual public key line on success.
-func CheckPublicKeyString(content string) (_ string, err error) {
-	if setting.SSH.Disabled {
-		return "", ErrSSHDisabled{}
-	}
-
-	content, err = parseKeyString(content)
-	if err != nil {
-		return "", err
-	}
-
-	content = strings.TrimRight(content, "\n\r")
-	if strings.ContainsAny(content, "\n\r") {
-		return "", errors.New("only a single line with a single key please")
-	}
-
-	// remove any unnecessary whitespace now
-	content = strings.TrimSpace(content)
-
-	if !setting.SSH.MinimumKeySizeCheck {
-		return content, nil
-	}
-
-	var (
-		fnName  string
-		keyType string
-		length  int
-	)
-	if setting.SSH.StartBuiltinServer {
-		fnName = "SSHNativeParsePublicKey"
-		keyType, length, err = SSHNativeParsePublicKey(content)
-	} else {
-		fnName = "SSHKeyGenParsePublicKey"
-		keyType, length, err = SSHKeyGenParsePublicKey(content)
-	}
-	if err != nil {
-		return "", fmt.Errorf("%s: %v", fnName, err)
-	}
-	log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
-
-	if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
-		return content, nil
-	} else if found && length < minLen {
-		return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
-	}
-	return "", fmt.Errorf("key type is not allowed: %s", keyType)
-}
-
-// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
-func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
-	// Don't need to rewrite this file if builtin SSH server is enabled.
-	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
-		return nil
-	}
-
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
-
-	if setting.SSH.RootPath != "" {
-		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
-		// This of course doesn't guarantee that this is the right directory for authorized_keys
-		// but at least if it's supposed to be this directory and it doesn't exist and we're the
-		// right user it will at least be created properly.
-		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
-		if err != nil {
-			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
-			return err
-		}
-	}
-
-	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
-	f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	// Note: chmod command does not support in Windows.
-	if !setting.IsWindows {
-		fi, err := f.Stat()
-		if err != nil {
-			return err
-		}
-
-		// .ssh directory should have mode 700, and authorized_keys file should have mode 600.
-		if fi.Mode().Perm() > 0o600 {
-			log.Error("authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
-			if err = f.Chmod(0o600); err != nil {
-				return err
-			}
-		}
-	}
-
-	for _, key := range keys {
-		if key.Type == KeyTypePrincipal {
-			continue
-		}
-		if _, err = f.WriteString(key.AuthorizedString()); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// checkKeyFingerprint only checks if key fingerprint has been used as public key,
-// it is OK to use same key as deploy key for multiple repositories/users.
-func checkKeyFingerprint(e Engine, fingerprint string) error {
-	has, err := e.Get(&PublicKey{
-		Fingerprint: fingerprint,
-	})
-	if err != nil {
-		return err
-	} else if has {
-		return ErrKeyAlreadyExist{0, fingerprint, ""}
-	}
-	return nil
-}
-
-func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
-	// Calculate fingerprint.
-	tmpPath, err := writeTmpKeyFile(publicKeyContent)
-	if err != nil {
-		return "", err
-	}
-	defer func() {
-		if err := util.Remove(tmpPath); err != nil {
-			log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
-		}
-	}()
-	stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
-	if err != nil {
-		if strings.Contains(stderr, "is not a public key file") {
-			return "", ErrKeyUnableVerify{stderr}
-		}
-		return "", fmt.Errorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
-	} else if len(stdout) < 2 {
-		return "", errors.New("not enough output for calculating fingerprint: " + stdout)
-	}
-	return strings.Split(stdout, " ")[1], nil
-}
-
-func calcFingerprintNative(publicKeyContent string) (string, error) {
-	// Calculate fingerprint.
-	pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
-	if err != nil {
-		return "", err
-	}
-	return ssh.FingerprintSHA256(pk), nil
-}
-
-func calcFingerprint(publicKeyContent string) (string, error) {
-	// Call the method based on configuration
-	var (
-		fnName, fp string
-		err        error
-	)
-	if setting.SSH.StartBuiltinServer {
-		fnName = "calcFingerprintNative"
-		fp, err = calcFingerprintNative(publicKeyContent)
-	} else {
-		fnName = "calcFingerprintSSHKeygen"
-		fp, err = calcFingerprintSSHKeygen(publicKeyContent)
-	}
-	if err != nil {
-		if IsErrKeyUnableVerify(err) {
-			log.Info("%s", publicKeyContent)
-			return "", err
-		}
-		return "", fmt.Errorf("%s: %v", fnName, err)
-	}
-	return fp, nil
+	return AuthorizedStringForKey(key)
 }
 
 func addKey(e Engine, key *PublicKey) (err error) {
@@ -782,603 +352,139 @@ func DeletePublicKey(doer *User, id int64) (err error) {
 	return RewriteAllPublicKeys()
 }
 
-// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
-// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
-// outside any session scope independently.
-func RewriteAllPublicKeys() error {
-	return rewriteAllPublicKeys(x)
-}
-
-func rewriteAllPublicKeys(e Engine) error {
-	// Don't rewrite key if internal server
-	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
-		return nil
-	}
-
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
-
-	if setting.SSH.RootPath != "" {
-		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
-		// This of course doesn't guarantee that this is the right directory for authorized_keys
-		// but at least if it's supposed to be this directory and it doesn't exist and we're the
-		// right user it will at least be created properly.
-		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
-		if err != nil {
-			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
-			return err
-		}
-	}
-
-	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
-	tmpPath := fPath + ".tmp"
-	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		t.Close()
-		if err := util.Remove(tmpPath); err != nil {
-			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
-		}
-	}()
-
-	if setting.SSH.AuthorizedKeysBackup {
-		isExist, err := util.IsExist(fPath)
-		if err != nil {
-			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-			return err
-		}
-		if isExist {
-			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
-			if err = util.CopyFile(fPath, bakPath); err != nil {
-				return err
-			}
-		}
-	}
-
-	if err := regeneratePublicKeys(e, t); err != nil {
-		return err
-	}
-
-	t.Close()
-	return os.Rename(tmpPath, fPath)
-}
-
-// RegeneratePublicKeys regenerates the authorized_keys file
-func RegeneratePublicKeys(t io.StringWriter) error {
-	return regeneratePublicKeys(x, t)
-}
-
-func regeneratePublicKeys(e Engine, t io.StringWriter) error {
-	if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
-		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
-		return err
-	}); err != nil {
-		return err
-	}
-
-	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
-	isExist, err := util.IsExist(fPath)
-	if err != nil {
-		log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-		return err
-	}
-	if isExist {
-		f, err := os.Open(fPath)
-		if err != nil {
-			return err
-		}
-		scanner := bufio.NewScanner(f)
-		for scanner.Scan() {
-			line := scanner.Text()
-			if strings.HasPrefix(line, tplCommentPrefix) {
-				scanner.Scan()
-				continue
-			}
-			_, err = t.WriteString(line + "\n")
-			if err != nil {
-				f.Close()
-				return err
-			}
-		}
-		f.Close()
-	}
-	return nil
-}
-
-// ________                .__                 ____  __.
-// \______ \   ____ ______ |  |   ____ ___.__.|    |/ _|____ ___.__.
-//  |    |  \_/ __ \\____ \|  |  /  _ <   |  ||      <_/ __ <   |  |
-//  |    `   \  ___/|  |_> >  |_(  <_> )___  ||    |  \  ___/\___  |
-// /_______  /\___  >   __/|____/\____// ____||____|__ \___  > ____|
-//         \/     \/|__|               \/             \/   \/\/
-
-// DeployKey represents deploy key information and its relation with repository.
-type DeployKey struct {
-	ID          int64 `xorm:"pk autoincr"`
-	KeyID       int64 `xorm:"UNIQUE(s) INDEX"`
-	RepoID      int64 `xorm:"UNIQUE(s) INDEX"`
-	Name        string
-	Fingerprint string
-	Content     string `xorm:"-"`
-
-	Mode AccessMode `xorm:"NOT NULL DEFAULT 1"`
-
-	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
-	UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
-	HasRecentActivity bool               `xorm:"-"`
-	HasUsed           bool               `xorm:"-"`
-}
-
-// AfterLoad is invoked from XORM after setting the values of all fields of this object.
-func (key *DeployKey) AfterLoad() {
-	key.HasUsed = key.UpdatedUnix > key.CreatedUnix
-	key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
-}
-
-// GetContent gets associated public key content.
-func (key *DeployKey) GetContent() error {
-	pkey, err := GetPublicKeyByID(key.KeyID)
-	if err != nil {
-		return err
-	}
-	key.Content = pkey.Content
-	return nil
-}
-
-// IsReadOnly checks if the key can only be used for read operations
-func (key *DeployKey) IsReadOnly() bool {
-	return key.Mode == AccessModeRead
-}
-
-func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
-	// Note: We want error detail, not just true or false here.
-	has, err := e.
-		Where("key_id = ? AND repo_id = ?", keyID, repoID).
-		Get(new(DeployKey))
-	if err != nil {
-		return err
-	} else if has {
-		return ErrDeployKeyAlreadyExist{keyID, repoID}
-	}
-
-	has, err = e.
-		Where("repo_id = ? AND name = ?", repoID, name).
-		Get(new(DeployKey))
-	if err != nil {
-		return err
-	} else if has {
-		return ErrDeployKeyNameAlreadyUsed{repoID, name}
-	}
-
-	return nil
-}
-
-// addDeployKey adds new key-repo relation.
-func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) {
-	if err := checkDeployKey(e, keyID, repoID, name); err != nil {
-		return nil, err
-	}
-
-	key := &DeployKey{
-		KeyID:       keyID,
-		RepoID:      repoID,
-		Name:        name,
-		Fingerprint: fingerprint,
-		Mode:        mode,
-	}
-	_, err := e.Insert(key)
-	return key, err
-}
-
-// HasDeployKey returns true if public key is a deploy key of given repository.
-func HasDeployKey(keyID, repoID int64) bool {
-	has, _ := x.
-		Where("key_id = ? AND repo_id = ?", keyID, repoID).
-		Get(new(DeployKey))
-	return has
-}
-
-// AddDeployKey add new deploy key to database and authorized_keys file.
-func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
-	fingerprint, err := calcFingerprint(content)
-	if err != nil {
-		return nil, err
-	}
-
-	accessMode := AccessModeRead
-	if !readOnly {
-		accessMode = AccessModeWrite
-	}
-
-	sess := x.NewSession()
-	defer sess.Close()
-	if err = sess.Begin(); err != nil {
-		return nil, err
-	}
-
-	pkey := &PublicKey{
-		Fingerprint: fingerprint,
-	}
-	has, err := sess.Get(pkey)
-	if err != nil {
-		return nil, err
-	}
-
-	if has {
-		if pkey.Type != KeyTypeDeploy {
-			return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
-		}
-	} else {
-		// First time use this deploy key.
-		pkey.Mode = accessMode
-		pkey.Type = KeyTypeDeploy
-		pkey.Content = content
-		pkey.Name = name
-		if err = addKey(sess, pkey); err != nil {
-			return nil, fmt.Errorf("addKey: %v", err)
-		}
-	}
-
-	key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
-	if err != nil {
-		return nil, err
-	}
-
-	return key, sess.Commit()
-}
-
-// GetDeployKeyByID returns deploy key by given ID.
-func GetDeployKeyByID(id int64) (*DeployKey, error) {
-	return getDeployKeyByID(x, id)
-}
-
-func getDeployKeyByID(e Engine, id int64) (*DeployKey, error) {
-	key := new(DeployKey)
-	has, err := e.ID(id).Get(key)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrDeployKeyNotExist{id, 0, 0}
-	}
-	return key, nil
-}
-
-// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
-func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
-	return getDeployKeyByRepo(x, keyID, repoID)
-}
-
-func getDeployKeyByRepo(e Engine, keyID, repoID int64) (*DeployKey, error) {
-	key := &DeployKey{
-		KeyID:  keyID,
-		RepoID: repoID,
-	}
-	has, err := e.Get(key)
-	if err != nil {
-		return nil, err
-	} else if !has {
-		return nil, ErrDeployKeyNotExist{0, keyID, repoID}
-	}
-	return key, nil
-}
-
-// UpdateDeployKeyCols updates deploy key information in the specified columns.
-func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
-	_, err := x.ID(key.ID).Cols(cols...).Update(key)
-	return err
-}
-
-// UpdateDeployKey updates deploy key information.
-func UpdateDeployKey(key *DeployKey) error {
-	_, err := x.ID(key.ID).AllCols().Update(key)
-	return err
-}
-
-// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
-func DeleteDeployKey(doer *User, id int64) error {
+// deleteKeysMarkedForDeletion returns true if ssh keys needs update
+func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
+	// Start session
 	sess := x.NewSession()
 	defer sess.Close()
 	if err := sess.Begin(); err != nil {
-		return err
-	}
-	if err := deleteDeployKey(sess, doer, id); err != nil {
-		return err
-	}
-	return sess.Commit()
-}
-
-func deleteDeployKey(sess Engine, doer *User, id int64) error {
-	key, err := getDeployKeyByID(sess, id)
-	if err != nil {
-		if IsErrDeployKeyNotExist(err) {
-			return nil
-		}
-		return fmt.Errorf("GetDeployKeyByID: %v", err)
+		return false, err
 	}
 
-	// Check if user has access to delete this key.
-	if !doer.IsAdmin {
-		repo, err := getRepositoryByID(sess, key.RepoID)
-		if err != nil {
-			return fmt.Errorf("GetRepositoryByID: %v", err)
-		}
-		has, err := isUserRepoAdmin(sess, repo, doer)
+	// Delete keys marked for deletion
+	var sshKeysNeedUpdate bool
+	for _, KeyToDelete := range keys {
+		key, err := searchPublicKeyByContentWithEngine(sess, KeyToDelete)
 		if err != nil {
-			return fmt.Errorf("GetUserRepoPermission: %v", err)
-		} else if !has {
-			return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
-		}
-	}
-
-	if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
-		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
-	}
-
-	// Check if this is the last reference to same key content.
-	has, err := sess.
-		Where("key_id = ?", key.KeyID).
-		Get(new(DeployKey))
-	if err != nil {
-		return err
-	} else if !has {
-		if err = deletePublicKeys(sess, key.KeyID); err != nil {
-			return err
+			log.Error("SearchPublicKeyByContent: %v", err)
+			continue
 		}
-
-		// after deleted the public keys, should rewrite the public keys file
-		if err = rewriteAllPublicKeys(sess); err != nil {
-			return err
+		if err = deletePublicKeys(sess, key.ID); err != nil {
+			log.Error("deletePublicKeys: %v", err)
+			continue
 		}
+		sshKeysNeedUpdate = true
 	}
 
-	return nil
-}
-
-// ListDeployKeys returns all deploy keys by given repository ID.
-func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
-	return listDeployKeys(x, repoID, listOptions)
-}
-
-func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
-	sess := e.Where("repo_id = ?", repoID)
-	if listOptions.Page != 0 {
-		sess = listOptions.setSessionPagination(sess)
-
-		keys := make([]*DeployKey, 0, listOptions.PageSize)
-		return keys, sess.Find(&keys)
-	}
-
-	keys := make([]*DeployKey, 0, 5)
-	return keys, sess.Find(&keys)
-}
-
-// SearchDeployKeys returns a list of deploy keys matching the provided arguments.
-func SearchDeployKeys(repoID, keyID int64, fingerprint string) ([]*DeployKey, error) {
-	keys := make([]*DeployKey, 0, 5)
-	cond := builder.NewCond()
-	if repoID != 0 {
-		cond = cond.And(builder.Eq{"repo_id": repoID})
-	}
-	if keyID != 0 {
-		cond = cond.And(builder.Eq{"key_id": keyID})
-	}
-	if fingerprint != "" {
-		cond = cond.And(builder.Eq{"fingerprint": fingerprint})
-	}
-	return keys, x.Where(cond).Find(&keys)
-}
-
-// __________       .__              .__             .__
-// \______   _______|__| ____   ____ |_____________  |  |   ______
-//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
-//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
-//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
-//                          \/     \/   |__|       \/          \/
-
-// AddPrincipalKey adds new principal to database and authorized_principals file.
-func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) {
-	sess := x.NewSession()
-	defer sess.Close()
-	if err := sess.Begin(); err != nil {
-		return nil, err
-	}
-
-	// Principals cannot be duplicated.
-	has, err := sess.
-		Where("content = ? AND type = ?", content, KeyTypePrincipal).
-		Get(new(PublicKey))
-	if err != nil {
-		return nil, err
-	} else if has {
-		return nil, ErrKeyAlreadyExist{0, "", content}
-	}
-
-	key := &PublicKey{
-		OwnerID:       ownerID,
-		Name:          content,
-		Content:       content,
-		Mode:          AccessModeWrite,
-		Type:          KeyTypePrincipal,
-		LoginSourceID: loginSourceID,
-	}
-	if err = addPrincipalKey(sess, key); err != nil {
-		return nil, fmt.Errorf("addKey: %v", err)
-	}
-
-	if err = sess.Commit(); err != nil {
-		return nil, err
-	}
-
-	sess.Close()
-
-	return key, RewriteAllPrincipalKeys()
-}
-
-func addPrincipalKey(e Engine, key *PublicKey) (err error) {
-	// Save Key representing a principal.
-	if _, err = e.Insert(key); err != nil {
-		return err
+	if err := sess.Commit(); err != nil {
+		return false, err
 	}
 
-	return nil
+	return sshKeysNeedUpdate, nil
 }
 
-// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
-func CheckPrincipalKeyString(user *User, content string) (_ string, err error) {
-	if setting.SSH.Disabled {
-		return "", ErrSSHDisabled{}
-	}
-
-	content = strings.TrimSpace(content)
-	if strings.ContainsAny(content, "\r\n") {
-		return "", errors.New("only a single line with a single principal please")
-	}
-
-	// check all the allowed principals, email, username or anything
-	// if any matches, return ok
-	for _, v := range setting.SSH.AuthorizedPrincipalsAllow {
-		switch v {
-		case "anything":
-			return content, nil
-		case "email":
-			emails, err := GetEmailAddresses(user.ID)
+// AddPublicKeysBySource add a users public keys. Returns true if there are changes.
+func AddPublicKeysBySource(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+	var sshKeysNeedUpdate bool
+	for _, sshKey := range sshPublicKeys {
+		var err error
+		found := false
+		keys := []byte(sshKey)
+	loop:
+		for len(keys) > 0 && err == nil {
+			var out ssh.PublicKey
+			// We ignore options as they are not relevant to Gitea
+			out, _, _, keys, err = ssh.ParseAuthorizedKey(keys)
 			if err != nil {
-				return "", err
+				break loop
 			}
-			for _, email := range emails {
-				if !email.IsActivated {
-					continue
-				}
-				if content == email.Email {
-					return content, nil
+			found = true
+			marshalled := string(ssh.MarshalAuthorizedKey(out))
+			marshalled = marshalled[:len(marshalled)-1]
+			sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
+
+			if _, err := AddPublicKey(usr.ID, sshKeyName, marshalled, s.ID); err != nil {
+				if IsErrKeyAlreadyExist(err) {
+					log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
+				} else {
+					log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
 				}
+			} else {
+				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
+				sshKeysNeedUpdate = true
 			}
-
-		case "username":
-			if content == user.Name {
-				return content, nil
-			}
+		}
+		if !found && err != nil {
+			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
 		}
 	}
-
-	return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
+	return sshKeysNeedUpdate
 }
 
-// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
-// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
-// outside any session scope independently.
-func RewriteAllPrincipalKeys() error {
-	return rewriteAllPrincipalKeys(x)
-}
+// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
+func SynchronizePublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
+	var sshKeysNeedUpdate bool
 
-func rewriteAllPrincipalKeys(e Engine) error {
-	// Don't rewrite key if internal server
-	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile {
-		return nil
-	}
-
-	sshOpLocker.Lock()
-	defer sshOpLocker.Unlock()
+	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
 
-	if setting.SSH.RootPath != "" {
-		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
-		// This of course doesn't guarantee that this is the right directory for authorized_keys
-		// but at least if it's supposed to be this directory and it doesn't exist and we're the
-		// right user it will at least be created properly.
-		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
-		if err != nil {
-			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
-			return err
-		}
+	// Get Public Keys from DB with current LDAP source
+	var giteaKeys []string
+	keys, err := ListPublicKeysBySource(usr.ID, s.ID)
+	if err != nil {
+		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
 	}
 
-	fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
-	tmpPath := fPath + ".tmp"
-	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
-	if err != nil {
-		return err
+	for _, v := range keys {
+		giteaKeys = append(giteaKeys, v.OmitEmail())
 	}
-	defer func() {
-		t.Close()
-		os.Remove(tmpPath)
-	}()
 
-	if setting.SSH.AuthorizedPrincipalsBackup {
-		isExist, err := util.IsExist(fPath)
-		if err != nil {
-			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-			return err
-		}
-		if isExist {
-			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
-			if err = util.CopyFile(fPath, bakPath); err != nil {
-				return err
+	// Process the provided keys to remove duplicates and name part
+	var providedKeys []string
+	for _, v := range sshPublicKeys {
+		sshKeySplit := strings.Split(v, " ")
+		if len(sshKeySplit) > 1 {
+			key := strings.Join(sshKeySplit[:2], " ")
+			if !util.ExistsInSlice(key, providedKeys) {
+				providedKeys = append(providedKeys, key)
 			}
 		}
 	}
 
-	if err := regeneratePrincipalKeys(e, t); err != nil {
-		return err
+	// Check if Public Key sync is needed
+	if util.IsEqualSlice(giteaKeys, providedKeys) {
+		log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
+		return false
 	}
+	log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
 
-	t.Close()
-	return os.Rename(tmpPath, fPath)
-}
-
-// ListPrincipalKeys returns a list of principals belongs to given user.
-func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
-	sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
-	if listOptions.Page != 0 {
-		sess = listOptions.setSessionPagination(sess)
-
-		keys := make([]*PublicKey, 0, listOptions.PageSize)
-		return keys, sess.Find(&keys)
+	// Add new Public SSH Keys that doesn't already exist in DB
+	var newKeys []string
+	for _, key := range providedKeys {
+		if !util.ExistsInSlice(key, giteaKeys) {
+			newKeys = append(newKeys, key)
+		}
+	}
+	if AddPublicKeysBySource(usr, s, newKeys) {
+		sshKeysNeedUpdate = true
 	}
 
-	keys := make([]*PublicKey, 0, 5)
-	return keys, sess.Find(&keys)
-}
-
-// RegeneratePrincipalKeys regenerates the authorized_principals file
-func RegeneratePrincipalKeys(t io.StringWriter) error {
-	return regeneratePrincipalKeys(x, t)
-}
-
-func regeneratePrincipalKeys(e Engine, t io.StringWriter) error {
-	if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
-		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
-		return err
-	}); err != nil {
-		return err
+	// Mark keys from DB that no longer exist in the source for deletion
+	var giteaKeysToDelete []string
+	for _, giteaKey := range giteaKeys {
+		if !util.ExistsInSlice(giteaKey, providedKeys) {
+			log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
+			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
+		}
 	}
 
-	fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
-	isExist, err := util.IsExist(fPath)
+	// Delete keys from DB that no longer exist in the source
+	needUpd, err := deleteKeysMarkedForDeletion(giteaKeysToDelete)
 	if err != nil {
-		log.Error("Unable to check if %s exists. Error: %v", fPath, err)
-		return err
+		log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
 	}
-	if isExist {
-		f, err := os.Open(fPath)
-		if err != nil {
-			return err
-		}
-		scanner := bufio.NewScanner(f)
-		for scanner.Scan() {
-			line := scanner.Text()
-			if strings.HasPrefix(line, tplCommentPrefix) {
-				scanner.Scan()
-				continue
-			}
-			_, err = t.WriteString(line + "\n")
-			if err != nil {
-				f.Close()
-				return err
-			}
-		}
-		f.Close()
+	if needUpd {
+		sshKeysNeedUpdate = true
 	}
-	return nil
+
+	return sshKeysNeedUpdate
 }
diff --git a/models/ssh_key_authorized_keys.go b/models/ssh_key_authorized_keys.go
new file mode 100644
index 0000000000000..21da7507de921
--- /dev/null
+++ b/models/ssh_key_authorized_keys.go
@@ -0,0 +1,218 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+)
+
+//  _____          __  .__                 .__                  .___
+// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
+// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
+// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
+// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
+//         \/                 \/                      \/    \/     \/
+// ____  __.
+// |    |/ _|____ ___.__. ______
+// |      <_/ __ <   |  |/  ___/
+// |    |  \  ___/\___  |\___ \
+// |____|__ \___  > ____/____  >
+//         \/   \/\/         \/
+//
+// This file contains functions for creating authorized_keys files
+//
+// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
+
+const (
+	tplCommentPrefix = `# gitea public key`
+	tplPublicKey     = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
+)
+
+var sshOpLocker sync.Mutex
+
+func AuthorizedStringForKey(key *PublicKey) string {
+	sb := &strings.Builder{}
+	_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]interface{}{
+		"AppPath":     util.ShellEscape(setting.AppPath),
+		"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
+		"CustomConf":  util.ShellEscape(setting.CustomConf),
+		"CustomPath":  util.ShellEscape(setting.CustomPath),
+		"Key":         key,
+	})
+
+	return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
+}
+
+// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
+func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
+	// Don't need to rewrite this file if builtin SSH server is enabled.
+	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
+		return nil
+	}
+
+	sshOpLocker.Lock()
+	defer sshOpLocker.Unlock()
+
+	if setting.SSH.RootPath != "" {
+		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
+		// This of course doesn't guarantee that this is the right directory for authorized_keys
+		// but at least if it's supposed to be this directory and it doesn't exist and we're the
+		// right user it will at least be created properly.
+		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+		if err != nil {
+			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+			return err
+		}
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+	f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	// Note: chmod command does not support in Windows.
+	if !setting.IsWindows {
+		fi, err := f.Stat()
+		if err != nil {
+			return err
+		}
+
+		// .ssh directory should have mode 700, and authorized_keys file should have mode 600.
+		if fi.Mode().Perm() > 0o600 {
+			log.Error("authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
+			if err = f.Chmod(0o600); err != nil {
+				return err
+			}
+		}
+	}
+
+	for _, key := range keys {
+		if key.Type == KeyTypePrincipal {
+			continue
+		}
+		if _, err = f.WriteString(key.AuthorizedString()); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
+// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
+// outside any session scope independently.
+func RewriteAllPublicKeys() error {
+	return rewriteAllPublicKeys(x)
+}
+
+func rewriteAllPublicKeys(e Engine) error {
+	// Don't rewrite key if internal server
+	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
+		return nil
+	}
+
+	sshOpLocker.Lock()
+	defer sshOpLocker.Unlock()
+
+	if setting.SSH.RootPath != "" {
+		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
+		// This of course doesn't guarantee that this is the right directory for authorized_keys
+		// but at least if it's supposed to be this directory and it doesn't exist and we're the
+		// right user it will at least be created properly.
+		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+		if err != nil {
+			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+			return err
+		}
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+	tmpPath := fPath + ".tmp"
+	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		t.Close()
+		if err := util.Remove(tmpPath); err != nil {
+			log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
+		}
+	}()
+
+	if setting.SSH.AuthorizedKeysBackup {
+		isExist, err := util.IsExist(fPath)
+		if err != nil {
+			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+			return err
+		}
+		if isExist {
+			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
+			if err = util.CopyFile(fPath, bakPath); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := regeneratePublicKeys(e, t); err != nil {
+		return err
+	}
+
+	t.Close()
+	return os.Rename(tmpPath, fPath)
+}
+
+// RegeneratePublicKeys regenerates the authorized_keys file
+func RegeneratePublicKeys(t io.StringWriter) error {
+	return regeneratePublicKeys(x, t)
+}
+
+func regeneratePublicKeys(e Engine, t io.StringWriter) error {
+	if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
+		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
+		return err
+	}); err != nil {
+		return err
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+	isExist, err := util.IsExist(fPath)
+	if err != nil {
+		log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+		return err
+	}
+	if isExist {
+		f, err := os.Open(fPath)
+		if err != nil {
+			return err
+		}
+		scanner := bufio.NewScanner(f)
+		for scanner.Scan() {
+			line := scanner.Text()
+			if strings.HasPrefix(line, tplCommentPrefix) {
+				scanner.Scan()
+				continue
+			}
+			_, err = t.WriteString(line + "\n")
+			if err != nil {
+				f.Close()
+				return err
+			}
+		}
+		f.Close()
+	}
+	return nil
+}
diff --git a/models/ssh_key_authorized_principals.go b/models/ssh_key_authorized_principals.go
new file mode 100644
index 0000000000000..9a763a3484040
--- /dev/null
+++ b/models/ssh_key_authorized_principals.go
@@ -0,0 +1,142 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+)
+
+//  _____          __  .__                 .__                  .___
+// /  _  \  __ ___/  |_|  |__   ___________|__|_______ ____   __| _/
+// /  /_\  \|  |  \   __\  |  \ /  _ \_  __ \  \___   // __ \ / __ |
+// /    |    \  |  /|  | |   Y  (  <_> )  | \/  |/    /\  ___// /_/ |
+// \____|__  /____/ |__| |___|  /\____/|__|  |__/_____ \\___  >____ |
+//         \/                 \/                      \/    \/     \/
+// __________       .__              .__             .__
+// \______   _______|__| ____   ____ |_____________  |  |   ______
+//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
+//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
+//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
+//                          \/     \/   |__|       \/          \/
+//
+// This file contains functions for creating authorized_principals files
+//
+// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
+// The sshOpLocker is used from ssh_key_authorized_keys.go
+
+const authorizedPrincipalsFile = "authorized_principals"
+
+// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
+// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
+// outside any session scope independently.
+func RewriteAllPrincipalKeys() error {
+	return rewriteAllPrincipalKeys(x)
+}
+
+func rewriteAllPrincipalKeys(e Engine) error {
+	// Don't rewrite key if internal server
+	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile {
+		return nil
+	}
+
+	sshOpLocker.Lock()
+	defer sshOpLocker.Unlock()
+
+	if setting.SSH.RootPath != "" {
+		// First of ensure that the RootPath is present, and if not make it with 0700 permissions
+		// This of course doesn't guarantee that this is the right directory for authorized_keys
+		// but at least if it's supposed to be this directory and it doesn't exist and we're the
+		// right user it will at least be created properly.
+		err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+		if err != nil {
+			log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+			return err
+		}
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
+	tmpPath := fPath + ".tmp"
+	t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		t.Close()
+		os.Remove(tmpPath)
+	}()
+
+	if setting.SSH.AuthorizedPrincipalsBackup {
+		isExist, err := util.IsExist(fPath)
+		if err != nil {
+			log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+			return err
+		}
+		if isExist {
+			bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
+			if err = util.CopyFile(fPath, bakPath); err != nil {
+				return err
+			}
+		}
+	}
+
+	if err := regeneratePrincipalKeys(e, t); err != nil {
+		return err
+	}
+
+	t.Close()
+	return os.Rename(tmpPath, fPath)
+}
+
+// RegeneratePrincipalKeys regenerates the authorized_principals file
+func RegeneratePrincipalKeys(t io.StringWriter) error {
+	return regeneratePrincipalKeys(x, t)
+}
+
+func regeneratePrincipalKeys(e Engine, t io.StringWriter) error {
+	if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
+		_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
+		return err
+	}); err != nil {
+		return err
+	}
+
+	fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
+	isExist, err := util.IsExist(fPath)
+	if err != nil {
+		log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+		return err
+	}
+	if isExist {
+		f, err := os.Open(fPath)
+		if err != nil {
+			return err
+		}
+		scanner := bufio.NewScanner(f)
+		for scanner.Scan() {
+			line := scanner.Text()
+			if strings.HasPrefix(line, tplCommentPrefix) {
+				scanner.Scan()
+				continue
+			}
+			_, err = t.WriteString(line + "\n")
+			if err != nil {
+				f.Close()
+				return err
+			}
+		}
+		f.Close()
+	}
+	return nil
+}
diff --git a/models/ssh_key_deploy.go b/models/ssh_key_deploy.go
new file mode 100644
index 0000000000000..3189bcf456a64
--- /dev/null
+++ b/models/ssh_key_deploy.go
@@ -0,0 +1,299 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/modules/timeutil"
+	"xorm.io/builder"
+	"xorm.io/xorm"
+)
+
+// ________                .__                 ____  __.
+// \______ \   ____ ______ |  |   ____ ___.__.|    |/ _|____ ___.__.
+//  |    |  \_/ __ \\____ \|  |  /  _ <   |  ||      <_/ __ <   |  |
+//  |    `   \  ___/|  |_> >  |_(  <_> )___  ||    |  \  ___/\___  |
+// /_______  /\___  >   __/|____/\____// ____||____|__ \___  > ____|
+//         \/     \/|__|               \/             \/   \/\/
+//
+// This file contains functions specific to DeployKeys
+
+// DeployKey represents deploy key information and its relation with repository.
+type DeployKey struct {
+	ID          int64 `xorm:"pk autoincr"`
+	KeyID       int64 `xorm:"UNIQUE(s) INDEX"`
+	RepoID      int64 `xorm:"UNIQUE(s) INDEX"`
+	Name        string
+	Fingerprint string
+	Content     string `xorm:"-"`
+
+	Mode AccessMode `xorm:"NOT NULL DEFAULT 1"`
+
+	CreatedUnix       timeutil.TimeStamp `xorm:"created"`
+	UpdatedUnix       timeutil.TimeStamp `xorm:"updated"`
+	HasRecentActivity bool               `xorm:"-"`
+	HasUsed           bool               `xorm:"-"`
+}
+
+// AfterLoad is invoked from XORM after setting the values of all fields of this object.
+func (key *DeployKey) AfterLoad() {
+	key.HasUsed = key.UpdatedUnix > key.CreatedUnix
+	key.HasRecentActivity = key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
+}
+
+// GetContent gets associated public key content.
+func (key *DeployKey) GetContent() error {
+	pkey, err := GetPublicKeyByID(key.KeyID)
+	if err != nil {
+		return err
+	}
+	key.Content = pkey.Content
+	return nil
+}
+
+// IsReadOnly checks if the key can only be used for read operations
+func (key *DeployKey) IsReadOnly() bool {
+	return key.Mode == AccessModeRead
+}
+
+func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
+	// Note: We want error detail, not just true or false here.
+	has, err := e.
+		Where("key_id = ? AND repo_id = ?", keyID, repoID).
+		Get(new(DeployKey))
+	if err != nil {
+		return err
+	} else if has {
+		return ErrDeployKeyAlreadyExist{keyID, repoID}
+	}
+
+	has, err = e.
+		Where("repo_id = ? AND name = ?", repoID, name).
+		Get(new(DeployKey))
+	if err != nil {
+		return err
+	} else if has {
+		return ErrDeployKeyNameAlreadyUsed{repoID, name}
+	}
+
+	return nil
+}
+
+// addDeployKey adds new key-repo relation.
+func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string, mode AccessMode) (*DeployKey, error) {
+	if err := checkDeployKey(e, keyID, repoID, name); err != nil {
+		return nil, err
+	}
+
+	key := &DeployKey{
+		KeyID:       keyID,
+		RepoID:      repoID,
+		Name:        name,
+		Fingerprint: fingerprint,
+		Mode:        mode,
+	}
+	_, err := e.Insert(key)
+	return key, err
+}
+
+// HasDeployKey returns true if public key is a deploy key of given repository.
+func HasDeployKey(keyID, repoID int64) bool {
+	has, _ := x.
+		Where("key_id = ? AND repo_id = ?", keyID, repoID).
+		Get(new(DeployKey))
+	return has
+}
+
+// AddDeployKey add new deploy key to database and authorized_keys file.
+func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey, error) {
+	fingerprint, err := calcFingerprint(content)
+	if err != nil {
+		return nil, err
+	}
+
+	accessMode := AccessModeRead
+	if !readOnly {
+		accessMode = AccessModeWrite
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+	if err = sess.Begin(); err != nil {
+		return nil, err
+	}
+
+	pkey := &PublicKey{
+		Fingerprint: fingerprint,
+	}
+	has, err := sess.Get(pkey)
+	if err != nil {
+		return nil, err
+	}
+
+	if has {
+		if pkey.Type != KeyTypeDeploy {
+			return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
+		}
+	} else {
+		// First time use this deploy key.
+		pkey.Mode = accessMode
+		pkey.Type = KeyTypeDeploy
+		pkey.Content = content
+		pkey.Name = name
+		if err = addKey(sess, pkey); err != nil {
+			return nil, fmt.Errorf("addKey: %v", err)
+		}
+	}
+
+	key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
+	if err != nil {
+		return nil, err
+	}
+
+	return key, sess.Commit()
+}
+
+// GetDeployKeyByID returns deploy key by given ID.
+func GetDeployKeyByID(id int64) (*DeployKey, error) {
+	return getDeployKeyByID(x, id)
+}
+
+func getDeployKeyByID(e Engine, id int64) (*DeployKey, error) {
+	key := new(DeployKey)
+	has, err := e.ID(id).Get(key)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrDeployKeyNotExist{id, 0, 0}
+	}
+	return key, nil
+}
+
+// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
+func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
+	return getDeployKeyByRepo(x, keyID, repoID)
+}
+
+func getDeployKeyByRepo(e Engine, keyID, repoID int64) (*DeployKey, error) {
+	key := &DeployKey{
+		KeyID:  keyID,
+		RepoID: repoID,
+	}
+	has, err := e.Get(key)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrDeployKeyNotExist{0, keyID, repoID}
+	}
+	return key, nil
+}
+
+// UpdateDeployKeyCols updates deploy key information in the specified columns.
+func UpdateDeployKeyCols(key *DeployKey, cols ...string) error {
+	_, err := x.ID(key.ID).Cols(cols...).Update(key)
+	return err
+}
+
+// UpdateDeployKey updates deploy key information.
+func UpdateDeployKey(key *DeployKey) error {
+	_, err := x.ID(key.ID).AllCols().Update(key)
+	return err
+}
+
+// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
+func DeleteDeployKey(doer *User, id int64) error {
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+	if err := deleteDeployKey(sess, doer, id); err != nil {
+		return err
+	}
+	return sess.Commit()
+}
+
+func deleteDeployKey(sess Engine, doer *User, id int64) error {
+	key, err := getDeployKeyByID(sess, id)
+	if err != nil {
+		if IsErrDeployKeyNotExist(err) {
+			return nil
+		}
+		return fmt.Errorf("GetDeployKeyByID: %v", err)
+	}
+
+	// Check if user has access to delete this key.
+	if !doer.IsAdmin {
+		repo, err := getRepositoryByID(sess, key.RepoID)
+		if err != nil {
+			return fmt.Errorf("GetRepositoryByID: %v", err)
+		}
+		has, err := isUserRepoAdmin(sess, repo, doer)
+		if err != nil {
+			return fmt.Errorf("GetUserRepoPermission: %v", err)
+		} else if !has {
+			return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
+		}
+	}
+
+	if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
+		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
+	}
+
+	// Check if this is the last reference to same key content.
+	has, err := sess.
+		Where("key_id = ?", key.KeyID).
+		Get(new(DeployKey))
+	if err != nil {
+		return err
+	} else if !has {
+		if err = deletePublicKeys(sess, key.KeyID); err != nil {
+			return err
+		}
+
+		// after deleted the public keys, should rewrite the public keys file
+		if err = rewriteAllPublicKeys(sess); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ListDeployKeys returns all deploy keys by given repository ID.
+func ListDeployKeys(repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
+	return listDeployKeys(x, repoID, listOptions)
+}
+
+func listDeployKeys(e Engine, repoID int64, listOptions ListOptions) ([]*DeployKey, error) {
+	sess := e.Where("repo_id = ?", repoID)
+	if listOptions.Page != 0 {
+		sess = listOptions.setSessionPagination(sess)
+
+		keys := make([]*DeployKey, 0, listOptions.PageSize)
+		return keys, sess.Find(&keys)
+	}
+
+	keys := make([]*DeployKey, 0, 5)
+	return keys, sess.Find(&keys)
+}
+
+// SearchDeployKeys returns a list of deploy keys matching the provided arguments.
+func SearchDeployKeys(repoID, keyID int64, fingerprint string) ([]*DeployKey, error) {
+	keys := make([]*DeployKey, 0, 5)
+	cond := builder.NewCond()
+	if repoID != 0 {
+		cond = cond.And(builder.Eq{"repo_id": repoID})
+	}
+	if keyID != 0 {
+		cond = cond.And(builder.Eq{"key_id": keyID})
+	}
+	if fingerprint != "" {
+		cond = cond.And(builder.Eq{"fingerprint": fingerprint})
+	}
+	return keys, x.Where(cond).Find(&keys)
+}
diff --git a/models/ssh_key_fingerprint.go b/models/ssh_key_fingerprint.go
new file mode 100644
index 0000000000000..96cc7d9c484f1
--- /dev/null
+++ b/models/ssh_key_fingerprint.go
@@ -0,0 +1,97 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/process"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+	"golang.org/x/crypto/ssh"
+)
+
+// ___________.__                                         .__        __
+// \_   _____/|__| ____    ____   ________________________|__| _____/  |_
+//  |    __)  |  |/    \  / ___\_/ __ \_  __ \____ \_  __ \  |/    \   __\
+//  |     \   |  |   |  \/ /_/  >  ___/|  | \/  |_> >  | \/  |   |  \  |
+//  \___  /   |__|___|  /\___  / \___  >__|  |   __/|__|  |__|___|  /__|
+//      \/            \//_____/      \/      |__|                 \/
+//
+// This file contains functions for fingerprinting SSH keys
+//
+// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
+
+// checkKeyFingerprint only checks if key fingerprint has been used as public key,
+// it is OK to use same key as deploy key for multiple repositories/users.
+func checkKeyFingerprint(e Engine, fingerprint string) error {
+	has, err := e.Get(&PublicKey{
+		Fingerprint: fingerprint,
+	})
+	if err != nil {
+		return err
+	} else if has {
+		return ErrKeyAlreadyExist{0, fingerprint, ""}
+	}
+	return nil
+}
+
+func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
+	// Calculate fingerprint.
+	tmpPath, err := writeTmpKeyFile(publicKeyContent)
+	if err != nil {
+		return "", err
+	}
+	defer func() {
+		if err := util.Remove(tmpPath); err != nil {
+			log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
+		}
+	}()
+	stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
+	if err != nil {
+		if strings.Contains(stderr, "is not a public key file") {
+			return "", ErrKeyUnableVerify{stderr}
+		}
+		return "", fmt.Errorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
+	} else if len(stdout) < 2 {
+		return "", errors.New("not enough output for calculating fingerprint: " + stdout)
+	}
+	return strings.Split(stdout, " ")[1], nil
+}
+
+func calcFingerprintNative(publicKeyContent string) (string, error) {
+	// Calculate fingerprint.
+	pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
+	if err != nil {
+		return "", err
+	}
+	return ssh.FingerprintSHA256(pk), nil
+}
+
+func calcFingerprint(publicKeyContent string) (string, error) {
+	// Call the method based on configuration
+	var (
+		fnName, fp string
+		err        error
+	)
+	if setting.SSH.StartBuiltinServer {
+		fnName = "calcFingerprintNative"
+		fp, err = calcFingerprintNative(publicKeyContent)
+	} else {
+		fnName = "calcFingerprintSSHKeygen"
+		fp, err = calcFingerprintSSHKeygen(publicKeyContent)
+	}
+	if err != nil {
+		if IsErrKeyUnableVerify(err) {
+			log.Info("%s", publicKeyContent)
+			return "", err
+		}
+		return "", fmt.Errorf("%s: %v", fnName, err)
+	}
+	return fp, nil
+}
diff --git a/models/ssh_key_parse.go b/models/ssh_key_parse.go
new file mode 100644
index 0000000000000..a86b7de02a8f8
--- /dev/null
+++ b/models/ssh_key_parse.go
@@ -0,0 +1,309 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/base64"
+	"encoding/binary"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/big"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/process"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
+	"golang.org/x/crypto/ssh"
+)
+
+//  ____  __.             __________
+// |    |/ _|____ ___.__. \______   \_____ _______  ______ ___________
+// |      <_/ __ <   |  |  |     ___/\__  \\_  __ \/  ___// __ \_  __ \
+// |    |  \  ___/\___  |  |    |     / __ \|  | \/\___ \\  ___/|  | \/
+// |____|__ \___  > ____|  |____|    (____  /__|  /____  >\___  >__|
+//         \/   \/\/                      \/           \/     \/
+//
+// This file contains functiosn for parsing ssh-keys
+//
+// TODO: Consider if these functions belong in models - no other models function call them or are called by them
+// They may belong in a service or a module
+
+const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----"
+
+func extractTypeFromBase64Key(key string) (string, error) {
+	b, err := base64.StdEncoding.DecodeString(key)
+	if err != nil || len(b) < 4 {
+		return "", fmt.Errorf("invalid key format: %v", err)
+	}
+
+	keyLength := int(binary.BigEndian.Uint32(b))
+	if len(b) < 4+keyLength {
+		return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
+	}
+
+	return string(b[4 : 4+keyLength]), nil
+}
+
+// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
+func parseKeyString(content string) (string, error) {
+	// remove whitespace at start and end
+	content = strings.TrimSpace(content)
+
+	var keyType, keyContent, keyComment string
+
+	if strings.HasPrefix(content, ssh2keyStart) {
+		// Parse SSH2 file format.
+
+		// Transform all legal line endings to a single "\n".
+		content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
+
+		lines := strings.Split(content, "\n")
+		continuationLine := false
+
+		for _, line := range lines {
+			// Skip lines that:
+			// 1) are a continuation of the previous line,
+			// 2) contain ":" as that are comment lines
+			// 3) contain "-" as that are begin and end tags
+			if continuationLine || strings.ContainsAny(line, ":-") {
+				continuationLine = strings.HasSuffix(line, "\\")
+			} else {
+				keyContent += line
+			}
+		}
+
+		t, err := extractTypeFromBase64Key(keyContent)
+		if err != nil {
+			return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
+		}
+		keyType = t
+	} else {
+		if strings.Contains(content, "-----BEGIN") {
+			// Convert PEM Keys to OpenSSH format
+			// Transform all legal line endings to a single "\n".
+			content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content)
+
+			block, _ := pem.Decode([]byte(content))
+			if block == nil {
+				return "", fmt.Errorf("failed to parse PEM block containing the public key")
+			}
+
+			pub, err := x509.ParsePKIXPublicKey(block.Bytes)
+			if err != nil {
+				var pk rsa.PublicKey
+				_, err2 := asn1.Unmarshal(block.Bytes, &pk)
+				if err2 != nil {
+					return "", fmt.Errorf("failed to parse DER encoded public key as either PKIX or PEM RSA Key: %v %v", err, err2)
+				}
+				pub = &pk
+			}
+
+			sshKey, err := ssh.NewPublicKey(pub)
+			if err != nil {
+				return "", fmt.Errorf("unable to convert to ssh public key: %v", err)
+			}
+			content = string(ssh.MarshalAuthorizedKey(sshKey))
+		}
+		// Parse OpenSSH format.
+
+		// Remove all newlines
+		content = strings.NewReplacer("\r\n", "", "\n", "").Replace(content)
+
+		parts := strings.SplitN(content, " ", 3)
+		switch len(parts) {
+		case 0:
+			return "", errors.New("empty key")
+		case 1:
+			keyContent = parts[0]
+		case 2:
+			keyType = parts[0]
+			keyContent = parts[1]
+		default:
+			keyType = parts[0]
+			keyContent = parts[1]
+			keyComment = parts[2]
+		}
+
+		// If keyType is not given, extract it from content. If given, validate it.
+		t, err := extractTypeFromBase64Key(keyContent)
+		if err != nil {
+			return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
+		}
+		if len(keyType) == 0 {
+			keyType = t
+		} else if keyType != t {
+			return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
+		}
+	}
+	// Finally we need to check whether we can actually read the proposed key:
+	_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyType + " " + keyContent + " " + keyComment))
+	if err != nil {
+		return "", fmt.Errorf("invalid ssh public key: %v", err)
+	}
+	return keyType + " " + keyContent + " " + keyComment, nil
+}
+
+// CheckPublicKeyString checks if the given public key string is recognized by SSH.
+// It returns the actual public key line on success.
+func CheckPublicKeyString(content string) (_ string, err error) {
+	if setting.SSH.Disabled {
+		return "", ErrSSHDisabled{}
+	}
+
+	content, err = parseKeyString(content)
+	if err != nil {
+		return "", err
+	}
+
+	content = strings.TrimRight(content, "\n\r")
+	if strings.ContainsAny(content, "\n\r") {
+		return "", errors.New("only a single line with a single key please")
+	}
+
+	// remove any unnecessary whitespace now
+	content = strings.TrimSpace(content)
+
+	if !setting.SSH.MinimumKeySizeCheck {
+		return content, nil
+	}
+
+	var (
+		fnName  string
+		keyType string
+		length  int
+	)
+	if setting.SSH.StartBuiltinServer {
+		fnName = "SSHNativeParsePublicKey"
+		keyType, length, err = SSHNativeParsePublicKey(content)
+	} else {
+		fnName = "SSHKeyGenParsePublicKey"
+		keyType, length, err = SSHKeyGenParsePublicKey(content)
+	}
+	if err != nil {
+		return "", fmt.Errorf("%s: %v", fnName, err)
+	}
+	log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
+
+	if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
+		return content, nil
+	} else if found && length < minLen {
+		return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
+	}
+	return "", fmt.Errorf("key type is not allowed: %s", keyType)
+}
+
+// SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
+func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
+	fields := strings.Fields(keyLine)
+	if len(fields) < 2 {
+		return "", 0, fmt.Errorf("not enough fields in public key line: %s", keyLine)
+	}
+
+	raw, err := base64.StdEncoding.DecodeString(fields[1])
+	if err != nil {
+		return "", 0, err
+	}
+
+	pkey, err := ssh.ParsePublicKey(raw)
+	if err != nil {
+		if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
+			return "", 0, ErrKeyUnableVerify{err.Error()}
+		}
+		return "", 0, fmt.Errorf("ParsePublicKey: %v", err)
+	}
+
+	// The ssh library can parse the key, so next we find out what key exactly we have.
+	switch pkey.Type() {
+	case ssh.KeyAlgoDSA:
+		rawPub := struct {
+			Name       string
+			P, Q, G, Y *big.Int
+		}{}
+		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
+			return "", 0, err
+		}
+		// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
+		// see dsa keys != 1024 bit, but as it seems to work, we will not check here
+		return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
+	case ssh.KeyAlgoRSA:
+		rawPub := struct {
+			Name string
+			E    *big.Int
+			N    *big.Int
+		}{}
+		if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
+			return "", 0, err
+		}
+		return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
+	case ssh.KeyAlgoECDSA256:
+		return "ecdsa", 256, nil
+	case ssh.KeyAlgoECDSA384:
+		return "ecdsa", 384, nil
+	case ssh.KeyAlgoECDSA521:
+		return "ecdsa", 521, nil
+	case ssh.KeyAlgoED25519:
+		return "ed25519", 256, nil
+	case ssh.KeyAlgoSKECDSA256:
+		return "ecdsa-sk", 256, nil
+	case ssh.KeyAlgoSKED25519:
+		return "ed25519-sk", 256, nil
+	}
+	return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
+}
+
+// writeTmpKeyFile writes key content to a temporary file
+// and returns the name of that file, along with any possible errors.
+func writeTmpKeyFile(content string) (string, error) {
+	tmpFile, err := ioutil.TempFile(setting.SSH.KeyTestPath, "gitea_keytest")
+	if err != nil {
+		return "", fmt.Errorf("TempFile: %v", err)
+	}
+	defer tmpFile.Close()
+
+	if _, err = tmpFile.WriteString(content); err != nil {
+		return "", fmt.Errorf("WriteString: %v", err)
+	}
+	return tmpFile.Name(), nil
+}
+
+// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
+func SSHKeyGenParsePublicKey(key string) (string, int, error) {
+	tmpName, err := writeTmpKeyFile(key)
+	if err != nil {
+		return "", 0, fmt.Errorf("writeTmpKeyFile: %v", err)
+	}
+	defer func() {
+		if err := util.Remove(tmpName); err != nil {
+			log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
+		}
+	}()
+
+	stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", setting.SSH.KeygenPath, "-lf", tmpName)
+	if err != nil {
+		return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
+	}
+	if strings.Contains(stdout, "is not a public key file") {
+		return "", 0, ErrKeyUnableVerify{stdout}
+	}
+
+	fields := strings.Split(stdout, " ")
+	if len(fields) < 4 {
+		return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
+	}
+
+	keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
+	length, err := strconv.ParseInt(fields[0], 10, 32)
+	if err != nil {
+		return "", 0, err
+	}
+	return strings.ToLower(keyType), int(length), nil
+}
diff --git a/models/ssh_key_principals.go b/models/ssh_key_principals.go
new file mode 100644
index 0000000000000..3459e43c8b042
--- /dev/null
+++ b/models/ssh_key_principals.go
@@ -0,0 +1,125 @@
+// Copyright 2021 The Gitea 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 models
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// __________       .__              .__             .__
+// \______   _______|__| ____   ____ |_____________  |  |   ______
+//  |     ___\_  __ |  |/    \_/ ___\|  \____ \__  \ |  |  /  ___/
+//  |    |    |  | \|  |   |  \  \___|  |  |_> / __ \|  |__\___ \
+//  |____|    |__|  |__|___|  /\___  |__|   __(____  |____/____  >
+//                          \/     \/   |__|       \/          \/
+//
+// This file contains functions related to principals
+
+// AddPrincipalKey adds new principal to database and authorized_principals file.
+func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) {
+	sess := x.NewSession()
+	defer sess.Close()
+	if err := sess.Begin(); err != nil {
+		return nil, err
+	}
+
+	// Principals cannot be duplicated.
+	has, err := sess.
+		Where("content = ? AND type = ?", content, KeyTypePrincipal).
+		Get(new(PublicKey))
+	if err != nil {
+		return nil, err
+	} else if has {
+		return nil, ErrKeyAlreadyExist{0, "", content}
+	}
+
+	key := &PublicKey{
+		OwnerID:       ownerID,
+		Name:          content,
+		Content:       content,
+		Mode:          AccessModeWrite,
+		Type:          KeyTypePrincipal,
+		LoginSourceID: loginSourceID,
+	}
+	if err = addPrincipalKey(sess, key); err != nil {
+		return nil, fmt.Errorf("addKey: %v", err)
+	}
+
+	if err = sess.Commit(); err != nil {
+		return nil, err
+	}
+
+	sess.Close()
+
+	return key, RewriteAllPrincipalKeys()
+}
+
+func addPrincipalKey(e Engine, key *PublicKey) (err error) {
+	// Save Key representing a principal.
+	if _, err = e.Insert(key); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
+func CheckPrincipalKeyString(user *User, content string) (_ string, err error) {
+	if setting.SSH.Disabled {
+		return "", ErrSSHDisabled{}
+	}
+
+	content = strings.TrimSpace(content)
+	if strings.ContainsAny(content, "\r\n") {
+		return "", errors.New("only a single line with a single principal please")
+	}
+
+	// check all the allowed principals, email, username or anything
+	// if any matches, return ok
+	for _, v := range setting.SSH.AuthorizedPrincipalsAllow {
+		switch v {
+		case "anything":
+			return content, nil
+		case "email":
+			emails, err := GetEmailAddresses(user.ID)
+			if err != nil {
+				return "", err
+			}
+			for _, email := range emails {
+				if !email.IsActivated {
+					continue
+				}
+				if content == email.Email {
+					return content, nil
+				}
+			}
+
+		case "username":
+			if content == user.Name {
+				return content, nil
+			}
+		}
+	}
+
+	return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
+}
+
+// ListPrincipalKeys returns a list of principals belongs to given user.
+func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
+	sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
+	if listOptions.Page != 0 {
+		sess = listOptions.setSessionPagination(sess)
+
+		keys := make([]*PublicKey, 0, listOptions.PageSize)
+		return keys, sess.Find(&keys)
+	}
+
+	keys := make([]*PublicKey, 0, 5)
+	return keys, sess.Find(&keys)
+}
diff --git a/models/user.go b/models/user.go
index e5d8f5fdaba19..dc44c7d217f46 100644
--- a/models/user.go
+++ b/models/user.go
@@ -34,7 +34,6 @@ import (
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/crypto/pbkdf2"
 	"golang.org/x/crypto/scrypt"
-	"golang.org/x/crypto/ssh"
 	"xorm.io/builder"
 )
 
@@ -1634,143 +1633,6 @@ func GetWatchedRepos(userID int64, private bool, listOptions ListOptions) ([]*Re
 	return repos, sess.Find(&repos)
 }
 
-// deleteKeysMarkedForDeletion returns true if ssh keys needs update
-func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
-	// Start session
-	sess := x.NewSession()
-	defer sess.Close()
-	if err := sess.Begin(); err != nil {
-		return false, err
-	}
-
-	// Delete keys marked for deletion
-	var sshKeysNeedUpdate bool
-	for _, KeyToDelete := range keys {
-		key, err := searchPublicKeyByContentWithEngine(sess, KeyToDelete)
-		if err != nil {
-			log.Error("SearchPublicKeyByContent: %v", err)
-			continue
-		}
-		if err = deletePublicKeys(sess, key.ID); err != nil {
-			log.Error("deletePublicKeys: %v", err)
-			continue
-		}
-		sshKeysNeedUpdate = true
-	}
-
-	if err := sess.Commit(); err != nil {
-		return false, err
-	}
-
-	return sshKeysNeedUpdate, nil
-}
-
-// AddPublicKeysBySource add a users public keys. Returns true if there are changes.
-func AddPublicKeysBySource(usr *User, s *LoginSource, sshPublicKeys []string) bool {
-	var sshKeysNeedUpdate bool
-	for _, sshKey := range sshPublicKeys {
-		var err error
-		found := false
-		keys := []byte(sshKey)
-	loop:
-		for len(keys) > 0 && err == nil {
-			var out ssh.PublicKey
-			// We ignore options as they are not relevant to Gitea
-			out, _, _, keys, err = ssh.ParseAuthorizedKey(keys)
-			if err != nil {
-				break loop
-			}
-			found = true
-			marshalled := string(ssh.MarshalAuthorizedKey(out))
-			marshalled = marshalled[:len(marshalled)-1]
-			sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out))
-
-			if _, err := AddPublicKey(usr.ID, sshKeyName, marshalled, s.ID); err != nil {
-				if IsErrKeyAlreadyExist(err) {
-					log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name)
-				} else {
-					log.Error("AddPublicKeysBySource[%s]: Error adding Public SSH Key for user %s: %v", sshKeyName, usr.Name, err)
-				}
-			} else {
-				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
-				sshKeysNeedUpdate = true
-			}
-		}
-		if !found && err != nil {
-			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
-		}
-	}
-	return sshKeysNeedUpdate
-}
-
-// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
-func SynchronizePublicKeys(usr *User, s *LoginSource, sshPublicKeys []string) bool {
-	var sshKeysNeedUpdate bool
-
-	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
-
-	// Get Public Keys from DB with current LDAP source
-	var giteaKeys []string
-	keys, err := ListPublicKeysBySource(usr.ID, s.ID)
-	if err != nil {
-		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, err)
-	}
-
-	for _, v := range keys {
-		giteaKeys = append(giteaKeys, v.OmitEmail())
-	}
-
-	// Process the provided keys to remove duplicates and name part
-	var providedKeys []string
-	for _, v := range sshPublicKeys {
-		sshKeySplit := strings.Split(v, " ")
-		if len(sshKeySplit) > 1 {
-			key := strings.Join(sshKeySplit[:2], " ")
-			if !util.ExistsInSlice(key, providedKeys) {
-				providedKeys = append(providedKeys, key)
-			}
-		}
-	}
-
-	// Check if Public Key sync is needed
-	if util.IsEqualSlice(giteaKeys, providedKeys) {
-		log.Trace("synchronizePublicKeys[%s]: Public Keys are already in sync for %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
-		return false
-	}
-	log.Trace("synchronizePublicKeys[%s]: Public Key needs update for user %s (Source:%v/DB:%v)", s.Name, usr.Name, len(providedKeys), len(giteaKeys))
-
-	// Add new Public SSH Keys that doesn't already exist in DB
-	var newKeys []string
-	for _, key := range providedKeys {
-		if !util.ExistsInSlice(key, giteaKeys) {
-			newKeys = append(newKeys, key)
-		}
-	}
-	if AddPublicKeysBySource(usr, s, newKeys) {
-		sshKeysNeedUpdate = true
-	}
-
-	// Mark keys from DB that no longer exist in the source for deletion
-	var giteaKeysToDelete []string
-	for _, giteaKey := range giteaKeys {
-		if !util.ExistsInSlice(giteaKey, providedKeys) {
-			log.Trace("synchronizePublicKeys[%s]: Marking Public SSH Key for deletion for user %s: %v", s.Name, usr.Name, giteaKey)
-			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
-		}
-	}
-
-	// Delete keys from DB that no longer exist in the source
-	needUpd, err := deleteKeysMarkedForDeletion(giteaKeysToDelete)
-	if err != nil {
-		log.Error("synchronizePublicKeys[%s]: Error deleting Public Keys marked for deletion for user %s: %v", s.Name, usr.Name, err)
-	}
-	if needUpd {
-		sshKeysNeedUpdate = true
-	}
-
-	return sshKeysNeedUpdate
-}
-
 // IterateUser iterate users
 func IterateUser(f func(user *User) error) error {
 	var start int

From bb1ff63793b5288be76ea9e0d31cbc0527329074 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 19 Jun 2021 20:50:05 +0100
Subject: [PATCH 07/44] Extract out login-sources from models

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/admin.go                                  |   8 +-
 cmd/admin_auth_ldap.go                        |  17 +-
 cmd/admin_auth_ldap_test.go                   |  99 ++++---
 models/login_source.go                        | 278 +++++-------------
 models/oauth2.go                              |  45 +--
 models/ssh_key.go                             |  12 +-
 models/ssh_key_authorized_keys.go             |   1 +
 modules/auth/ldap/ldap.go                     |  12 +
 routers/web/admin/auths.go                    |  36 ++-
 routers/web/user/auth.go                      |   7 +-
 routers/web/user/setting/security.go          |   3 +-
 services/auth/interface.go                    |  11 +
 services/auth/signin.go                       | 109 +++----
 services/auth/source/db/login.go              |  38 +++
 services/auth/source/ldap/source.go           |  84 ++++++
 .../ldap/{login.go => source_authenticate.go} |  27 +-
 .../source/ldap/{sync.go => source_sync.go}   |  24 +-
 services/auth/source/oauth2/providers.go      |  38 +++
 services/auth/source/oauth2/source.go         |  44 +++
 .../auth/source/oauth2/source_register.go     |  26 ++
 services/auth/source/pam/source.go            |  39 +++
 .../pam/{login.go => source_authenticate.go}  |  19 +-
 services/auth/source/smtp/auth.go             |  10 +-
 services/auth/source/smtp/source.go           |  58 ++++
 .../smtp/{login.go => source_authenticate.go} |  27 +-
 services/auth/source/sspi/source.go           |  42 +++
 services/auth/sspi_windows.go                 |  11 +-
 services/auth/sync.go                         |   5 +-
 28 files changed, 657 insertions(+), 473 deletions(-)
 create mode 100644 services/auth/source/db/login.go
 create mode 100644 services/auth/source/ldap/source.go
 rename services/auth/source/ldap/{login.go => source_authenticate.go} (67%)
 rename services/auth/source/ldap/{sync.go => source_sync.go} (86%)
 create mode 100644 services/auth/source/oauth2/providers.go
 create mode 100644 services/auth/source/oauth2/source.go
 create mode 100644 services/auth/source/oauth2/source_register.go
 create mode 100644 services/auth/source/pam/source.go
 rename services/auth/source/pam/{login.go => source_authenticate.go} (70%)
 create mode 100644 services/auth/source/smtp/source.go
 rename services/auth/source/smtp/{login.go => source_authenticate.go} (62%)
 create mode 100644 services/auth/source/sspi/source.go

diff --git a/cmd/admin.go b/cmd/admin.go
index f58a1f99607af..da04e181275d7 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -15,6 +15,8 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth/oauth2"
+	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
+
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
@@ -597,7 +599,7 @@ func runRegenerateKeys(_ *cli.Context) error {
 	return models.RewriteAllPublicKeys()
 }
 
-func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
+func parseOAuth2Config(c *cli.Context) *oauth2Service.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if c.IsSet("use-custom-urls") {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -609,7 +611,7 @@ func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
 	} else {
 		customURLMapping = nil
 	}
-	return &models.OAuth2Config{
+	return &oauth2Service.Source{
 		Provider:                      c.String("provider"),
 		ClientID:                      c.String("key"),
 		ClientSecret:                  c.String("secret"),
@@ -646,7 +648,7 @@ func runUpdateOauth(c *cli.Context) error {
 		return err
 	}
 
-	oAuth2Config := source.OAuth2()
+	oAuth2Config := source.Cfg.(*oauth2Service.Source)
 
 	if c.IsSet("name") {
 		source.Name = c.String("name")
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index 5ab64ec7d53c5..aab26082e90b6 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -10,6 +10,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth/ldap"
+	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/urfave/cli"
 )
@@ -180,7 +181,7 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
 }
 
 // parseLdapConfig assigns values on config according to command line flags.
-func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
+func parseLdapConfig(c *cli.Context, config *ldapService.Source) error {
 	if c.IsSet("name") {
 		config.Source.Name = c.String("name")
 	}
@@ -251,7 +252,7 @@ func parseLdapConfig(c *cli.Context, config *models.LDAPConfig) error {
 // findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
 // It returns the value of the security protocol and if it was found.
 func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
-	for i, n := range models.SecurityProtocolNames {
+	for i, n := range ldap.SecurityProtocolNames {
 		if strings.EqualFold(name, n) {
 			return i, true
 		}
@@ -291,7 +292,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 	loginSource := &models.LoginSource{
 		Type:      models.LoginLDAP,
 		IsActived: true, // active by default
-		Cfg: &models.LDAPConfig{
+		Cfg: &ldapService.Source{
 			Source: &ldap.Source{
 				Enabled: true, // always true
 			},
@@ -299,7 +300,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
 		return err
 	}
 
@@ -318,7 +319,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
 		return err
 	}
 
@@ -338,7 +339,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 	loginSource := &models.LoginSource{
 		Type:      models.LoginDLDAP,
 		IsActived: true, // active by default
-		Cfg: &models.LDAPConfig{
+		Cfg: &ldapService.Source{
 			Source: &ldap.Source{
 				Enabled: true, // always true
 			},
@@ -346,7 +347,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
 		return err
 	}
 
@@ -365,7 +366,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
 		return err
 	}
 
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index 87f4f789ab0ab..d051feee5bcd5 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth/ldap"
+	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/urfave/cli"
@@ -56,7 +57,7 @@ func TestAddLdapBindDn(t *testing.T) {
 				Name:          "ldap (via Bind DN) source full",
 				IsActived:     false,
 				IsSyncEnabled: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:                  "ldap (via Bind DN) source full",
 						Host:                  "ldap-bind-server full",
@@ -97,7 +98,7 @@ func TestAddLdapBindDn(t *testing.T) {
 				Type:      models.LoginLDAP,
 				Name:      "ldap (via Bind DN) source min",
 				IsActived: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:             "ldap (via Bind DN) source min",
 						Host:             "ldap-bind-server min",
@@ -279,7 +280,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source full",
 				IsActived: false,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:                  "ldap (simple auth) source full",
 						Host:                  "ldap-simple-server full",
@@ -317,7 +318,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source min",
 				IsActived: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:             "ldap (simple auth) source min",
 						Host:             "ldap-simple-server min",
@@ -518,7 +519,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Enabled: true,
 					},
@@ -529,7 +530,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				Name:          "ldap (via Bind DN) source full",
 				IsActived:     false,
 				IsSyncEnabled: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:                  "ldap (via Bind DN) source full",
 						Host:                  "ldap-bind-server full",
@@ -562,7 +563,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -577,7 +578,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
 				Name: "ldap (via Bind DN) source",
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name: "ldap (via Bind DN) source",
 					},
@@ -594,14 +595,14 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
 			loginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: false,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -615,7 +616,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						SecurityProtocol: ldap.SecurityProtocol(1),
 					},
@@ -631,7 +632,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						SkipVerify: true,
 					},
@@ -647,7 +648,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Host: "ldap-server",
 					},
@@ -663,7 +664,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Port: 389,
 					},
@@ -679,7 +680,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						UserBase: "ou=Users,dc=domain,dc=org",
 					},
@@ -695,7 +696,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
 					},
@@ -711,7 +712,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 					},
@@ -727,7 +728,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeUsername: "uid",
 					},
@@ -743,7 +744,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeName: "givenName",
 					},
@@ -759,7 +760,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeSurname: "sn",
 					},
@@ -775,7 +776,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeMail: "mail",
 					},
@@ -791,7 +792,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributesInBind: true,
 					},
@@ -807,7 +808,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeSSHPublicKey: "publickey",
 					},
@@ -823,7 +824,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						BindDN: "cn=readonly,dc=domain,dc=org",
 					},
@@ -839,7 +840,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						BindPassword: "secret",
 					},
@@ -856,7 +857,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				IsSyncEnabled: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -870,7 +871,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						SearchPageSize: 12,
 					},
@@ -901,7 +902,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginOAuth2,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -933,7 +934,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginLDAP,
-					Cfg: &models.LDAPConfig{
+					Cfg: &ldapService.Source{
 						Source: &ldap.Source{},
 					},
 				}, nil
@@ -997,7 +998,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source full",
 				IsActived: false,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name:                  "ldap (simple auth) source full",
 						Host:                  "ldap-simple-server full",
@@ -1026,7 +1027,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -1041,7 +1042,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
 				Name: "ldap (simple auth) source",
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Name: "ldap (simple auth) source",
 					},
@@ -1058,14 +1059,14 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginDLDAP,
 				IsActived: true,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
 			loginSource: &models.LoginSource{
 				Type:      models.LoginDLDAP,
 				IsActived: false,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -1079,7 +1080,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						SecurityProtocol: ldap.SecurityProtocol(2),
 					},
@@ -1095,7 +1096,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						SkipVerify: true,
 					},
@@ -1111,7 +1112,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Host: "ldap-server",
 					},
@@ -1127,7 +1128,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Port: 987,
 					},
@@ -1143,7 +1144,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						UserBase: "ou=Users,dc=domain,dc=org",
 					},
@@ -1159,7 +1160,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						Filter: "(&(objectClass=posixAccount)(cn=%s))",
 					},
@@ -1175,7 +1176,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 					},
@@ -1191,7 +1192,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeUsername: "uid",
 					},
@@ -1207,7 +1208,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeName: "givenName",
 					},
@@ -1223,7 +1224,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeSurname: "sn",
 					},
@@ -1239,7 +1240,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeMail: "mail",
 					},
@@ -1255,7 +1256,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						AttributeSSHPublicKey: "publickey",
 					},
@@ -1271,7 +1272,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{
 						UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
 					},
@@ -1302,7 +1303,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginPAM,
-				Cfg: &models.LDAPConfig{
+				Cfg: &ldapService.Source{
 					Source: &ldap.Source{},
 				},
 			},
@@ -1334,7 +1335,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginDLDAP,
-					Cfg: &models.LDAPConfig{
+					Cfg: &ldapService.Source{
 						Source: &ldap.Source{},
 					},
 				}, nil
diff --git a/models/login_source.go b/models/login_source.go
index 0ac24d680a571..b7b734b4b72e1 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -6,16 +6,12 @@
 package models
 
 import (
-	"fmt"
+	"reflect"
 	"strconv"
 
-	"code.gitea.io/gitea/modules/auth/ldap"
 	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/secret"
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
-	jsoniter "github.com/json-iterator/go"
 
 	"xorm.io/xorm"
 	"xorm.io/xorm/convert"
@@ -36,6 +32,11 @@ const (
 	LoginSSPI             // 7
 )
 
+// String returns the string name of the LoginType
+func (typ LoginType) String() string {
+	return LoginNames[typ]
+}
+
 // LoginNames contains the name of LoginType values.
 var LoginNames = map[LoginType]string{
 	LoginLDAP:   "LDAP (via BindDN)",
@@ -46,141 +47,42 @@ var LoginNames = map[LoginType]string{
 	LoginSSPI:   "SPNEGO with SSPI",
 }
 
-// SecurityProtocolNames contains the name of SecurityProtocol values.
-var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
-	ldap.SecurityProtocolUnencrypted: "Unencrypted",
-	ldap.SecurityProtocolLDAPS:       "LDAPS",
-	ldap.SecurityProtocolStartTLS:    "StartTLS",
+// LoginConfig represents login config as far as the db is concerned
+type LoginConfig interface {
+	convert.Conversion
 }
 
-// Ensure structs implemented interface.
-var (
-	_ convert.Conversion = &LDAPConfig{}
-	_ convert.Conversion = &SMTPConfig{}
-	_ convert.Conversion = &PAMConfig{}
-	_ convert.Conversion = &OAuth2Config{}
-	_ convert.Conversion = &SSPIConfig{}
-)
-
-// LDAPConfig holds configuration for LDAP login source.
-type LDAPConfig struct {
-	*ldap.Source
+// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
+type SkipVerifiable interface {
+	IsSkipVerify() bool
 }
 
-// FromDB fills up a LDAPConfig from serialized format.
-func (cfg *LDAPConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	err := json.Unmarshal(bs, &cfg)
-	if err != nil {
-		return err
-	}
-	if cfg.BindPasswordEncrypt != "" {
-		cfg.BindPassword, err = secret.DecryptSecret(setting.SecretKey, cfg.BindPasswordEncrypt)
-		cfg.BindPasswordEncrypt = ""
-	}
-	return err
+// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
+type HasTLSer interface {
+	HasTLS() bool
 }
 
-// ToDB exports a LDAPConfig to a serialized format.
-func (cfg *LDAPConfig) ToDB() ([]byte, error) {
-	var err error
-	cfg.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, cfg.BindPassword)
-	if err != nil {
-		return nil, err
-	}
-	cfg.BindPassword = ""
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
-}
-
-// SecurityProtocolName returns the name of configured security
-// protocol.
-func (cfg *LDAPConfig) SecurityProtocolName() string {
-	return SecurityProtocolNames[cfg.SecurityProtocol]
-}
-
-// SMTPConfig holds configuration for the SMTP login source.
-type SMTPConfig struct {
-	Auth           string
-	Host           string
-	Port           int
-	AllowedDomains string `xorm:"TEXT"`
-	TLS            bool
-	SkipVerify     bool
+// UseTLSer configurations provide a HasTLS to check if TLS is enabled
+type UseTLSer interface {
+	UseTLS() bool
 }
 
-// FromDB fills up an SMTPConfig from serialized format.
-func (cfg *SMTPConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, cfg)
+// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
+type SSHKeyProvider interface {
+	ProvidesSSHKeys() bool
 }
 
-// ToDB exports an SMTPConfig to a serialized format.
-func (cfg *SMTPConfig) ToDB() ([]byte, error) {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
+// RegisterableSource configurations provide RegisterSource which needs to be run on creation
+type RegisterableSource interface {
+	RegisterSource(*LoginSource) error
 }
 
-// PAMConfig holds configuration for the PAM login source.
-type PAMConfig struct {
-	ServiceName string // pam service (e.g. system-auth)
-	EmailDomain string
+// RegisterLoginTypeConfig register a config for a provided type
+func RegisterLoginTypeConfig(typ LoginType, config LoginConfig) {
+	registeredLoginConfigs[typ] = config
 }
 
-// FromDB fills up a PAMConfig from serialized format.
-func (cfg *PAMConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
-}
-
-// ToDB exports a PAMConfig to a serialized format.
-func (cfg *PAMConfig) ToDB() ([]byte, error) {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
-}
-
-// OAuth2Config holds configuration for the OAuth2 login source.
-type OAuth2Config struct {
-	Provider                      string
-	ClientID                      string
-	ClientSecret                  string
-	OpenIDConnectAutoDiscoveryURL string
-	CustomURLMapping              *oauth2.CustomURLMapping
-	IconURL                       string
-}
-
-// FromDB fills up an OAuth2Config from serialized format.
-func (cfg *OAuth2Config) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, cfg)
-}
-
-// ToDB exports an SMTPConfig to a serialized format.
-func (cfg *OAuth2Config) ToDB() ([]byte, error) {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
-}
-
-// SSPIConfig holds configuration for SSPI single sign-on.
-type SSPIConfig struct {
-	AutoCreateUsers      bool
-	AutoActivateUsers    bool
-	StripDomainNames     bool
-	SeparatorReplacement string
-	DefaultLanguage      string
-}
-
-// FromDB fills up an SSPIConfig from serialized format.
-func (cfg *SSPIConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, cfg)
-}
-
-// ToDB exports an SSPIConfig to a serialized format.
-func (cfg *SSPIConfig) ToDB() ([]byte, error) {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
-}
+var registeredLoginConfigs = map[LoginType]LoginConfig{}
 
 // LoginSource represents an external way for authorizing users.
 type LoginSource struct {
@@ -211,19 +113,11 @@ func Cell2Int64(val xorm.Cell) int64 {
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	if colName == "type" {
-		switch LoginType(Cell2Int64(val)) {
-		case LoginLDAP, LoginDLDAP:
-			source.Cfg = new(LDAPConfig)
-		case LoginSMTP:
-			source.Cfg = new(SMTPConfig)
-		case LoginPAM:
-			source.Cfg = new(PAMConfig)
-		case LoginOAuth2:
-			source.Cfg = new(OAuth2Config)
-		case LoginSSPI:
-			source.Cfg = new(SSPIConfig)
-		default:
-			panic(fmt.Sprintf("unrecognized login source type: %v", *val))
+		typ := LoginType(Cell2Int64(val))
+		exemplar, ok := registeredLoginConfigs[typ]
+		if ok {
+			source.Cfg = reflect.New(reflect.TypeOf(exemplar)).Interface().(convert.Conversion)
+			return
 		}
 	}
 }
@@ -265,59 +159,21 @@ func (source *LoginSource) IsSSPI() bool {
 
 // HasTLS returns true of this source supports TLS.
 func (source *LoginSource) HasTLS() bool {
-	return ((source.IsLDAP() || source.IsDLDAP()) &&
-		source.LDAP().SecurityProtocol > ldap.SecurityProtocolUnencrypted) ||
-		source.IsSMTP()
+	hasTLSer, ok := source.Cfg.(HasTLSer)
+	return ok && hasTLSer.HasTLS()
 }
 
 // UseTLS returns true of this source is configured to use TLS.
 func (source *LoginSource) UseTLS() bool {
-	switch source.Type {
-	case LoginLDAP, LoginDLDAP:
-		return source.LDAP().SecurityProtocol != ldap.SecurityProtocolUnencrypted
-	case LoginSMTP:
-		return source.SMTP().TLS
-	}
-
-	return false
+	useTLSer, ok := source.Cfg.(UseTLSer)
+	return ok && useTLSer.UseTLS()
 }
 
 // SkipVerify returns true if this source is configured to skip SSL
 // verification.
 func (source *LoginSource) SkipVerify() bool {
-	switch source.Type {
-	case LoginLDAP, LoginDLDAP:
-		return source.LDAP().SkipVerify
-	case LoginSMTP:
-		return source.SMTP().SkipVerify
-	}
-
-	return false
-}
-
-// LDAP returns LDAPConfig for this source, if of LDAP type.
-func (source *LoginSource) LDAP() *LDAPConfig {
-	return source.Cfg.(*LDAPConfig)
-}
-
-// SMTP returns SMTPConfig for this source, if of SMTP type.
-func (source *LoginSource) SMTP() *SMTPConfig {
-	return source.Cfg.(*SMTPConfig)
-}
-
-// PAM returns PAMConfig for this source, if of PAM type.
-func (source *LoginSource) PAM() *PAMConfig {
-	return source.Cfg.(*PAMConfig)
-}
-
-// OAuth2 returns OAuth2Config for this source, if of OAuth2 type.
-func (source *LoginSource) OAuth2() *OAuth2Config {
-	return source.Cfg.(*OAuth2Config)
-}
-
-// SSPI returns SSPIConfig for this source, if of SSPI type.
-func (source *LoginSource) SSPI() *SSPIConfig {
-	return source.Cfg.(*SSPIConfig)
+	skipVerifiable, ok := source.Cfg.(SkipVerifiable)
+	return ok && skipVerifiable.IsSkipVerify()
 }
 
 // CreateLoginSource inserts a LoginSource in the DB if not already
@@ -335,16 +191,24 @@ func CreateLoginSource(source *LoginSource) error {
 	}
 
 	_, err = x.Insert(source)
-	if err == nil && source.IsOAuth2() && source.IsActived {
-		oAuth2Config := source.OAuth2()
-		err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
-		err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config)
-		if err != nil {
-			// remove the LoginSource in case of errors while registering OAuth2 providers
-			if _, err := x.Delete(source); err != nil {
-				log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
-			}
-			return err
+	if err != nil {
+		return err
+	}
+
+	if !source.IsActived {
+		return nil
+	}
+
+	registerableSource, ok := source.Cfg.(RegisterableSource)
+	if !ok {
+		return nil
+	}
+
+	err = registerableSource.RegisterSource(source)
+	if err != nil {
+		// remove the LoginSource in case of errors while registering configuration
+		if _, err := x.Delete(source); err != nil {
+			log.Error("CreateLoginSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 		}
 	}
 	return err
@@ -419,16 +283,24 @@ func UpdateSource(source *LoginSource) error {
 	}
 
 	_, err := x.ID(source.ID).AllCols().Update(source)
-	if err == nil && source.IsOAuth2() && source.IsActived {
-		oAuth2Config := source.OAuth2()
-		err = oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
-		err = wrapOpenIDConnectInitializeError(err, source.Name, oAuth2Config)
-		if err != nil {
-			// restore original values since we cannot update the provider it self
-			if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
-				log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
-			}
-			return err
+	if err != nil {
+		return err
+	}
+
+	if !source.IsActived {
+		return nil
+	}
+
+	registerableSource, ok := source.Cfg.(RegisterableSource)
+	if !ok {
+		return nil
+	}
+
+	err = registerableSource.RegisterSource(source)
+	if err != nil {
+		// restore original values since we cannot update the provider it self
+		if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
+			log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 		}
 	}
 	return err
diff --git a/models/oauth2.go b/models/oauth2.go
index 46da60e02dd65..e2e6f849bedcd 100644
--- a/models/oauth2.go
+++ b/models/oauth2.go
@@ -5,8 +5,6 @@
 package models
 
 import (
-	"sort"
-
 	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/log"
 )
@@ -103,33 +101,6 @@ func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
 	return loginSource, nil
 }
 
-// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
-// key is used as technical name (like in the callbackURL)
-// values to display
-func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {
-	// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
-
-	loginSources, err := GetActiveOAuth2ProviderLoginSources()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	var orderedKeys []string
-	providers := make(map[string]OAuth2Provider)
-	for _, source := range loginSources {
-		prov := OAuth2Providers[source.OAuth2().Provider]
-		if source.OAuth2().IconURL != "" {
-			prov.Image = source.OAuth2().IconURL
-		}
-		providers[source.Name] = prov
-		orderedKeys = append(orderedKeys, source.Name)
-	}
-
-	sort.Strings(orderedKeys)
-
-	return orderedKeys, providers, nil
-}
-
 // InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
 func InitOAuth2() error {
 	if err := oauth2.InitSigningKey(); err != nil {
@@ -151,8 +122,11 @@ func ResetOAuth2() error {
 func initOAuth2LoginSources() error {
 	loginSources, _ := GetActiveOAuth2ProviderLoginSources()
 	for _, source := range loginSources {
-		oAuth2Config := source.OAuth2()
-		err := oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
+		registerableSource, ok := source.Cfg.(RegisterableSource)
+		if !ok {
+			continue
+		}
+		err := registerableSource.RegisterSource(source)
 		if err != nil {
 			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
 			source.IsActived = false
@@ -164,12 +138,3 @@ func initOAuth2LoginSources() error {
 	}
 	return nil
 }
-
-// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
-// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
-func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *OAuth2Config) error {
-	if err != nil && "openidConnect" == oAuth2Config.Provider {
-		err = ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err}
-	}
-	return err
-}
diff --git a/models/ssh_key.go b/models/ssh_key.go
index fd8fa660683d4..6cda4f1658fb2 100644
--- a/models/ssh_key.go
+++ b/models/ssh_key.go
@@ -278,11 +278,7 @@ keyloop:
 			}
 		}
 
-		ldapSource := source.LDAP()
-		if ldapSource != nil &&
-			source.IsSyncEnabled &&
-			(source.Type == LoginLDAP || source.Type == LoginDLDAP) &&
-			len(strings.TrimSpace(ldapSource.AttributeSSHPublicKey)) > 0 {
+		if sshKeyProvider, ok := source.Cfg.(SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
 			// Disable setting SSH keys for this user
 			externals[i] = true
 		}
@@ -307,11 +303,7 @@ func PublicKeyIsExternallyManaged(id int64) (bool, error) {
 		}
 		return false, err
 	}
-	ldapSource := source.LDAP()
-	if ldapSource != nil &&
-		source.IsSyncEnabled &&
-		(source.Type == LoginLDAP || source.Type == LoginDLDAP) &&
-		len(strings.TrimSpace(ldapSource.AttributeSSHPublicKey)) > 0 {
+	if sshKeyProvider, ok := source.Cfg.(SSHKeyProvider); ok && sshKeyProvider.ProvidesSSHKeys() {
 		// Disable setting SSH keys for this user
 		return true, nil
 	}
diff --git a/models/ssh_key_authorized_keys.go b/models/ssh_key_authorized_keys.go
index 21da7507de921..15cdbdb19edfb 100644
--- a/models/ssh_key_authorized_keys.go
+++ b/models/ssh_key_authorized_keys.go
@@ -43,6 +43,7 @@ const (
 
 var sshOpLocker sync.Mutex
 
+// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
 func AuthorizedStringForKey(key *PublicKey) string {
 	sb := &strings.Builder{}
 	_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]interface{}{
diff --git a/modules/auth/ldap/ldap.go b/modules/auth/ldap/ldap.go
index 91ad33a60f3a4..321fbf805891a 100644
--- a/modules/auth/ldap/ldap.go
+++ b/modules/auth/ldap/ldap.go
@@ -27,6 +27,18 @@ const (
 	SecurityProtocolStartTLS
 )
 
+// String returns the name of the SecurityProtocol
+func (s SecurityProtocol) String() string {
+	return SecurityProtocolNames[s]
+}
+
+// SecurityProtocolNames contains the name of SecurityProtocol values.
+var SecurityProtocolNames = map[SecurityProtocol]string{
+	SecurityProtocolUnencrypted: "Unencrypted",
+	SecurityProtocolLDAPS:       "LDAPS",
+	SecurityProtocolStartTLS:    "StartTLS",
+}
+
 // Source Basic LDAP authentication service
 type Source struct {
 	Name                  string // canonical name (ie. corporate.ad)
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 2c9f215d1d8db..dd3036213ac6d 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -20,7 +20,11 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
+	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
+	pamService "code.gitea.io/gitea/services/auth/source/pam"
 	"code.gitea.io/gitea/services/auth/source/smtp"
+	"code.gitea.io/gitea/services/auth/source/sspi"
 	"code.gitea.io/gitea/services/forms"
 
 	"xorm.io/xorm/convert"
@@ -75,9 +79,9 @@ var (
 	}()
 
 	securityProtocols = []dropdownItem{
-		{models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
-		{models.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
-		{models.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
+		{ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
+		{ldap.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
+		{ldap.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
 	}
 )
 
@@ -89,7 +93,7 @@ func NewAuthSource(ctx *context.Context) {
 
 	ctx.Data["type"] = models.LoginLDAP
 	ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginLDAP]
-	ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
+	ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
 	ctx.Data["smtp_auth"] = "PLAIN"
 	ctx.Data["is_active"] = true
 	ctx.Data["is_sync_enabled"] = true
@@ -114,12 +118,12 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplAuthNew)
 }
 
-func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
+func parseLDAPConfig(form forms.AuthenticationForm) *ldapService.Source {
 	var pageSize uint32
 	if form.UsePagedSearch {
 		pageSize = uint32(form.SearchPageSize)
 	}
-	return &models.LDAPConfig{
+	return &ldapService.Source{
 		Source: &ldap.Source{
 			Name:                  form.Name,
 			Host:                  form.Host,
@@ -151,8 +155,8 @@ func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
 	}
 }
 
-func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
-	return &models.SMTPConfig{
+func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
+	return &smtp.Source{
 		Auth:           form.SMTPAuth,
 		Host:           form.SMTPHost,
 		Port:           form.SMTPPort,
@@ -162,7 +166,7 @@ func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
 	}
 }
 
-func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
+func parseOAuth2Config(form forms.AuthenticationForm) *oauth2Service.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if form.Oauth2UseCustomURL {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -174,7 +178,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 	} else {
 		customURLMapping = nil
 	}
-	return &models.OAuth2Config{
+	return &oauth2Service.Source{
 		Provider:                      form.Oauth2Provider,
 		ClientID:                      form.Oauth2Key,
 		ClientSecret:                  form.Oauth2Secret,
@@ -184,7 +188,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 	}
 }
 
-func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*models.SSPIConfig, error) {
+func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
 	if util.IsEmptyString(form.SSPISeparatorReplacement) {
 		ctx.Data["Err_SSPISeparatorReplacement"] = true
 		return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
@@ -199,7 +203,7 @@ func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*mode
 		return nil, errors.New(ctx.Tr("form.lang_select_error"))
 	}
 
-	return &models.SSPIConfig{
+	return &sspi.Source{
 		AutoCreateUsers:      form.SSPIAutoCreateUsers,
 		AutoActivateUsers:    form.SSPIAutoActivateUsers,
 		StripDomainNames:     form.SSPIStripDomainNames,
@@ -216,7 +220,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 	ctx.Data["PageIsAdminAuthentications"] = true
 
 	ctx.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(form.Type)]
-	ctx.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
+	ctx.Data["CurrentSecurityProtocol"] = ldap.SecurityProtocolNames[ldap.SecurityProtocol(form.SecurityProtocol)]
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
@@ -239,7 +243,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 		config = parseSMTPConfig(form)
 		hasTLS = true
 	case models.LoginPAM:
-		config = &models.PAMConfig{
+		config = &pamService.Source{
 			ServiceName: form.PAMServiceName,
 			EmailDomain: form.PAMEmailDomain,
 		}
@@ -311,7 +315,7 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["HasTLS"] = source.HasTLS()
 
 	if source.IsOAuth2() {
-		ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.OAuth2().Provider]
+		ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.Cfg.(*oauth2Service.Source).Provider]
 	}
 	ctx.HTML(http.StatusOK, tplAuthEdit)
 }
@@ -347,7 +351,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 	case models.LoginSMTP:
 		config = parseSMTPConfig(form)
 	case models.LoginPAM:
-		config = &models.PAMConfig{
+		config = &pamService.Source{
 			ServiceName: form.PAMServiceName,
 			EmailDomain: form.PAMEmailDomain,
 		}
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 9309a111cd463..5034347bb4178 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -28,6 +28,7 @@ import (
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/utils"
 	"code.gitea.io/gitea/services/auth"
+	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -136,7 +137,7 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
-	orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+	orderedOAuth2Names, oauth2Providers, err := oauth2Service.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -156,7 +157,7 @@ func SignIn(ctx *context.Context) {
 func SignInPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 
-	orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+	orderedOAuth2Names, oauth2Providers, err := oauth2Service.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -632,7 +633,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 			if len(missingFields) > 0 {
 				log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
-				if loginSource.IsOAuth2() && loginSource.OAuth2().Provider == "openidConnect" {
+				if loginSource.IsOAuth2() && loginSource.Cfg.(*oauth2Service.Source).Provider == "openidConnect" {
 					log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
 				}
 				err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
diff --git a/routers/web/user/setting/security.go b/routers/web/user/setting/security.go
index 7753c5c16179d..d1c61a9a4c4f2 100644
--- a/routers/web/user/setting/security.go
+++ b/routers/web/user/setting/security.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 )
 
 const (
@@ -92,7 +93,7 @@ func loadSecurityData(ctx *context.Context) {
 		if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
 			var providerDisplayName string
 			if loginSource.IsOAuth2() {
-				providerTechnicalName := loginSource.OAuth2().Provider
+				providerTechnicalName := loginSource.Cfg.(*oauth2.Source).Provider
 				providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
 			} else {
 				providerDisplayName = loginSource.Name
diff --git a/services/auth/interface.go b/services/auth/interface.go
index e75a84677c2b1..0488a8b716356 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -5,6 +5,7 @@
 package auth
 
 import (
+	"context"
 	"net/http"
 
 	"code.gitea.io/gitea/models"
@@ -37,3 +38,13 @@ type Method interface {
 	// Returns nil if verification fails.
 	Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
 }
+
+// Authenticator represents a source of authentication
+type Authenticator interface {
+	Authenticate(user *models.User, login, password string, source *models.LoginSource) (*models.User, error)
+}
+
+// SynchronizableSource represents a source that can synchronize users
+type SynchronizableSource interface {
+	Sync(ctx context.Context, updateExisting bool, source *models.LoginSource) error
+}
diff --git a/services/auth/signin.go b/services/auth/signin.go
index b49761e3a60aa..22529a4b9e5fa 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -9,10 +9,14 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/services/auth/source/ldap"
-	"code.gitea.io/gitea/services/auth/source/pam"
-	"code.gitea.io/gitea/services/auth/source/smtp"
+	"code.gitea.io/gitea/services/auth/source/db"
+
+	// Register the other sources
+	_ "code.gitea.io/gitea/services/auth/source/ldap"
+	_ "code.gitea.io/gitea/services/auth/source/oauth2"
+	_ "code.gitea.io/gitea/services/auth/source/pam"
+	_ "code.gitea.io/gitea/services/auth/source/smtp"
+	_ "code.gitea.io/gitea/services/auth/source/sspi"
 )
 
 // UserSignIn validates user name and password.
@@ -47,39 +51,35 @@ func UserSignIn(username, password string) (*models.User, error) {
 	if hasUser {
 		switch user.LoginType {
 		case models.LoginNoType, models.LoginPlain, models.LoginOAuth2:
-			if user.IsPasswordSet() && user.ValidatePassword(password) {
-
-				// Update password hash if server password hash algorithm have changed
-				if user.PasswdHashAlgo != setting.PasswordHashAlgo {
-					if err = user.SetPassword(password); err != nil {
-						return nil, err
-					}
-					if err = models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
-						return nil, err
-					}
-				}
-
-				// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
-				// user could be hint to resend confirm email.
-				if user.ProhibitLogin {
-					return nil, models.ErrUserProhibitLogin{
-						UID:  user.ID,
-						Name: user.Name,
-					}
-				}
-
-				return user, nil
+			return db.Authenticate(user, user.Name, password)
+		default:
+			source, err := models.GetLoginSourceByID(user.LoginSource)
+			if err != nil {
+				return nil, err
 			}
 
-			return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
+			if !source.IsActived {
+				return nil, models.ErrLoginSourceNotActived
+			}
 
-		default:
-			source, err := models.GetLoginSourceByID(user.LoginSource)
+			authenticator, ok := source.Cfg.(Authenticator)
+			if !ok {
+				return nil, models.ErrUnsupportedLoginType
+
+			}
+
+			user, err := authenticator.Authenticate(nil, username, password, source)
 			if err != nil {
 				return nil, err
 			}
 
-			return ExternalUserLogin(user, user.LoginName, password, source)
+			// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
+			// user could be hint to resend confirm email.
+			if user.ProhibitLogin {
+				return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
+			}
+
+			return user, nil
 		}
 	}
 
@@ -89,13 +89,23 @@ func UserSignIn(username, password string) (*models.User, error) {
 	}
 
 	for _, source := range sources {
-		if source.IsOAuth2() || source.IsSSPI() {
-			// don't try to authenticate against OAuth2 and SSPI sources here
+		if !source.IsActived {
+			// don't try to authenticate non-active sources
+			continue
+		}
+
+		authenticator, ok := source.Cfg.(Authenticator)
+		if !ok {
 			continue
 		}
-		authUser, err := ExternalUserLogin(nil, username, password, source)
+
+		authUser, err := authenticator.Authenticate(nil, username, password, source)
+
 		if err == nil {
-			return authUser, nil
+			if !user.ProhibitLogin {
+				return authUser, nil
+			}
+			err = models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
 		}
 
 		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
@@ -103,34 +113,3 @@ func UserSignIn(username, password string) (*models.User, error) {
 
 	return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
 }
-
-// ExternalUserLogin attempts a login using external source types.
-func ExternalUserLogin(user *models.User, login, password string, source *models.LoginSource) (*models.User, error) {
-	if !source.IsActived {
-		return nil, models.ErrLoginSourceNotActived
-	}
-
-	var err error
-	switch source.Type {
-	case models.LoginLDAP, models.LoginDLDAP:
-		user, err = ldap.Login(user, login, password, source)
-	case models.LoginSMTP:
-		user, err = smtp.Login(user, login, password, source.ID, source.Cfg.(*models.SMTPConfig))
-	case models.LoginPAM:
-		user, err = pam.Login(user, login, password, source.ID, source.Cfg.(*models.PAMConfig))
-	default:
-		return nil, models.ErrUnsupportedLoginType
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
-	// user could be hint to resend confirm email.
-	if user.ProhibitLogin {
-		return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
-	}
-
-	return user, nil
-}
diff --git a/services/auth/source/db/login.go b/services/auth/source/db/login.go
new file mode 100644
index 0000000000000..dc85fb2e89e57
--- /dev/null
+++ b/services/auth/source/db/login.go
@@ -0,0 +1,38 @@
+// Copyright 2021 The Gitea 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 db
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// Authenticate authenticates the provided user against the DB
+func Authenticate(user *models.User, login, password string) (*models.User, error) {
+	if !user.IsPasswordSet() || !user.ValidatePassword(password) {
+		return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
+	}
+
+	// Update password hash if server password hash algorithm have changed
+	if user.PasswdHashAlgo != setting.PasswordHashAlgo {
+		if err := user.SetPassword(password); err != nil {
+			return nil, err
+		}
+		if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil {
+			return nil, err
+		}
+	}
+
+	// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
+	// user could be hint to resend confirm email.
+	if user.ProhibitLogin {
+		return nil, models.ErrUserProhibitLogin{
+			UID:  user.ID,
+			Name: user.Name,
+		}
+	}
+
+	return user, nil
+}
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
new file mode 100644
index 0000000000000..bf0c69154e41f
--- /dev/null
+++ b/services/auth/source/ldap/source.go
@@ -0,0 +1,84 @@
+// Copyright 2021 The Gitea 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 (
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/ldap"
+	"code.gitea.io/gitea/modules/secret"
+	"code.gitea.io/gitea/modules/setting"
+	jsoniter "github.com/json-iterator/go"
+)
+
+// .____     ________      _____ __________
+// |    |    \______ \    /  _  \\______   \
+// |    |     |    |  \  /  /_\  \|     ___/
+// |    |___  |    `   \/    |    \    |
+// |_______ \/_______  /\____|__  /____|
+//         \/        \/         \/
+
+// Source holds configuration for LDAP login source.
+type Source struct {
+	*ldap.Source
+}
+
+// FromDB fills up a LDAPConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	err := json.Unmarshal(bs, &source)
+	if err != nil {
+		return err
+	}
+	if source.BindPasswordEncrypt != "" {
+		source.BindPassword, err = secret.DecryptSecret(setting.SecretKey, source.BindPasswordEncrypt)
+		source.BindPasswordEncrypt = ""
+	}
+	return err
+}
+
+// ToDB exports a LDAPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	var err error
+	source.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, source.BindPassword)
+	if err != nil {
+		return nil, err
+	}
+	source.BindPassword = ""
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Marshal(source)
+}
+
+// SecurityProtocolName returns the name of configured security
+// protocol.
+func (source *Source) SecurityProtocolName() string {
+	return ldap.SecurityProtocolNames[source.SecurityProtocol]
+}
+
+// IsSkipVerify returns if SkipVerify is set
+func (source *Source) IsSkipVerify() bool {
+	return source.SkipVerify
+}
+
+// HasTLS returns if HasTLS
+func (source *Source) HasTLS() bool {
+	return source.SecurityProtocol > ldap.SecurityProtocolUnencrypted
+}
+
+// UseTLS returns if UseTLS
+func (source *Source) UseTLS() bool {
+	return source.SecurityProtocol != ldap.SecurityProtocolUnencrypted
+}
+
+// ProvidesSSHKeys returns if this source provides SSH Keys
+func (source *Source) ProvidesSSHKeys() bool {
+	return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginLDAP, &Source{})
+	models.RegisterLoginTypeConfig(models.LoginDLDAP, &Source{})
+}
diff --git a/services/auth/source/ldap/login.go b/services/auth/source/ldap/source_authenticate.go
similarity index 67%
rename from services/auth/source/ldap/login.go
rename to services/auth/source/ldap/source_authenticate.go
index 266e4f85cf155..29325c2e83226 100644
--- a/services/auth/source/ldap/login.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -11,23 +11,16 @@ import (
 	"code.gitea.io/gitea/models"
 )
 
-// .____     ________      _____ __________
-// |    |    \______ \    /  _  \\______   \
-// |    |     |    |  \  /  /_\  \|     ___/
-// |    |___  |    `   \/    |    \    |
-// |_______ \/_______  /\____|__  /____|
-//         \/        \/         \/
-
-// Login queries if login/password is valid against the LDAP directory pool,
+// Authenticate queries if login/password is valid against the LDAP directory pool,
 // and create a local user if success when enabled.
-func Login(user *models.User, login, password string, source *models.LoginSource) (*models.User, error) {
-	sr := source.Cfg.(*models.LDAPConfig).SearchEntry(login, password, source.Type == models.LoginDLDAP)
+func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
+	sr := source.SearchEntry(login, password, loginSource.Type == models.LoginDLDAP)
 	if sr == nil {
 		// User not in LDAP, do nothing
 		return nil, models.ErrUserNotExist{Name: login}
 	}
 
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.LDAP().AttributeSSHPublicKey)) > 0
+	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 
 	// Update User admin flag if exist
 	if isExist, err := models.IsUserExist(0, sr.Username); err != nil {
@@ -41,12 +34,12 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 		}
 		if user != nil && !user.ProhibitLogin {
 			cols := make([]string, 0)
-			if len(source.LDAP().AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
+			if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
 				// Change existing admin flag only if AdminFilter option is set
 				user.IsAdmin = sr.IsAdmin
 				cols = append(cols, "is_admin")
 			}
-			if !user.IsAdmin && len(source.LDAP().RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
+			if !user.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
 				// Change existing restricted flag only if RestrictedFilter option is set
 				user.IsRestricted = sr.IsRestricted
 				cols = append(cols, "is_restricted")
@@ -61,7 +54,7 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 	}
 
 	if user != nil {
-		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source, sr.SSHPublicKey) {
+		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, loginSource, sr.SSHPublicKey) {
 			return user, models.RewriteAllPublicKeys()
 		}
 
@@ -82,8 +75,8 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 		Name:         sr.Username,
 		FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
 		Email:        sr.Mail,
-		LoginType:    source.Type,
-		LoginSource:  source.ID,
+		LoginType:    loginSource.Type,
+		LoginSource:  loginSource.ID,
 		LoginName:    login,
 		IsActive:     true,
 		IsAdmin:      sr.IsAdmin,
@@ -92,7 +85,7 @@ func Login(user *models.User, login, password string, source *models.LoginSource
 
 	err := models.CreateUser(user)
 
-	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source, sr.SSHPublicKey) {
+	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, loginSource, sr.SSHPublicKey) {
 		err = models.RewriteAllPublicKeys()
 	}
 
diff --git a/services/auth/source/ldap/sync.go b/services/auth/source/ldap/source_sync.go
similarity index 86%
rename from services/auth/source/ldap/sync.go
rename to services/auth/source/ldap/source_sync.go
index e102a8c2deb5f..a91269bc09ea4 100644
--- a/services/auth/source/ldap/sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -13,18 +13,12 @@ import (
 	"code.gitea.io/gitea/modules/log"
 )
 
-// .____     ________      _____ __________
-// |    |    \______ \    /  _  \\______   \
-// |    |     |    |  \  /  /_\  \|     ___/
-// |    |___  |    `   \/    |    \    |
-// |_______ \/_______  /\____|__  /____|
-//         \/        \/         \/
-
-func Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error {
+// Sync causes this ldap source to synchronize its users with the db
+func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error {
 	log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
 
 	var existingUsers []int64
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(s.LDAP().AttributeSSHPublicKey)) > 0
+	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 	var sshKeysNeedUpdate bool
 
 	// Find all users with this login type - FIXME: Should this be an iterator?
@@ -40,14 +34,14 @@ func Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error
 	default:
 	}
 
-	sr, err := s.LDAP().SearchEntries()
+	sr, err := source.SearchEntries()
 	if err != nil {
 		log.Error("SyncExternalUsers LDAP source failure [%s], skipped", s.Name)
 		return nil
 	}
 
 	if len(sr) == 0 {
-		if !s.LDAP().AllowDeactivateAll {
+		if !source.AllowDeactivateAll {
 			log.Error("LDAP search found no entries but did not report an error. Refusing to deactivate all users")
 			return nil
 		}
@@ -122,8 +116,8 @@ func Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error
 			}
 
 			// Check if user data has changed
-			if (len(s.LDAP().AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
-				(len(s.LDAP().RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
+			if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
+				(len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
 				!strings.EqualFold(usr.Email, su.Mail) ||
 				usr.FullName != fullName ||
 				!usr.IsActive {
@@ -133,11 +127,11 @@ func Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error
 				usr.FullName = fullName
 				usr.Email = su.Mail
 				// Change existing admin flag only if AdminFilter option is set
-				if len(s.LDAP().AdminFilter) > 0 {
+				if len(source.AdminFilter) > 0 {
 					usr.IsAdmin = su.IsAdmin
 				}
 				// Change existing restricted flag only if RestrictedFilter option is set
-				if !usr.IsAdmin && len(s.LDAP().RestrictedFilter) > 0 {
+				if !usr.IsAdmin && len(source.RestrictedFilter) > 0 {
 					usr.IsRestricted = su.IsRestricted
 				}
 				usr.IsActive = true
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
new file mode 100644
index 0000000000000..69340cd09567a
--- /dev/null
+++ b/services/auth/source/oauth2/providers.go
@@ -0,0 +1,38 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"sort"
+
+	"code.gitea.io/gitea/models"
+)
+
+// GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
+// key is used as technical name (like in the callbackURL)
+// values to display
+func GetActiveOAuth2Providers() ([]string, map[string]models.OAuth2Provider, error) {
+	// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
+
+	loginSources, err := models.GetActiveOAuth2ProviderLoginSources()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var orderedKeys []string
+	providers := make(map[string]models.OAuth2Provider)
+	for _, source := range loginSources {
+		prov := models.OAuth2Providers[source.Cfg.(*Source).Provider]
+		if source.Cfg.(*Source).IconURL != "" {
+			prov.Image = source.Cfg.(*Source).IconURL
+		}
+		providers[source.Name] = prov
+		orderedKeys = append(orderedKeys, source.Name)
+	}
+
+	sort.Strings(orderedKeys)
+
+	return orderedKeys, providers, nil
+}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
new file mode 100644
index 0000000000000..81e899244a4c9
--- /dev/null
+++ b/services/auth/source/oauth2/source.go
@@ -0,0 +1,44 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/oauth2"
+	jsoniter "github.com/json-iterator/go"
+)
+
+// ________      _____          __  .__     ________
+// \_____  \    /  _  \  __ ___/  |_|  |__  \_____  \
+// /   |   \  /  /_\  \|  |  \   __\  |  \  /  ____/
+// /    |    \/    |    \  |  /|  | |   Y  \/       \
+// \_______  /\____|__  /____/ |__| |___|  /\_______ \
+//         \/         \/                 \/         \/
+
+// Source holds configuration for the OAuth2 login source.
+type Source struct {
+	Provider                      string
+	ClientID                      string
+	ClientSecret                  string
+	OpenIDConnectAutoDiscoveryURL string
+	CustomURLMapping              *oauth2.CustomURLMapping
+	IconURL                       string
+}
+
+// FromDB fills up an OAuth2Config from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Unmarshal(bs, source)
+}
+
+// ToDB exports an SMTPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Marshal(source)
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginOAuth2, &Source{})
+}
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
new file mode 100644
index 0000000000000..cc901c2f5de34
--- /dev/null
+++ b/services/auth/source/oauth2/source_register.go
@@ -0,0 +1,26 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth/oauth2"
+)
+
+// RegisterSource causes an OAuth2 configuration to be registered
+func (source *Source) RegisterSource(loginSource *models.LoginSource) error {
+
+	err := oauth2.RegisterProvider(loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
+	return wrapOpenIDConnectInitializeError(err, loginSource.Name, source)
+}
+
+// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
+// inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
+func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *Source) error {
+	if err != nil && oAuth2Config.Provider == "openidConnect" {
+		err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err}
+	}
+	return err
+}
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
new file mode 100644
index 0000000000000..f6b098e562af3
--- /dev/null
+++ b/services/auth/source/pam/source.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gitea 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 (
+	"code.gitea.io/gitea/models"
+	jsoniter "github.com/json-iterator/go"
+)
+
+// __________  _____      _____
+// \______   \/  _  \    /     \
+//  |     ___/  /_\  \  /  \ /  \
+//  |    |  /    |    \/    Y    \
+//  |____|  \____|__  /\____|__  /
+//                  \/         \/
+
+// Source holds configuration for the PAM login source.
+type Source struct {
+	ServiceName string // pam service (e.g. system-auth)
+	EmailDomain string
+}
+
+// FromDB fills up a PAMConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Unmarshal(bs, &source)
+}
+
+// ToDB exports a PAMConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Marshal(source)
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginPAM, &Source{})
+}
diff --git a/services/auth/source/pam/login.go b/services/auth/source/pam/source_authenticate.go
similarity index 70%
rename from services/auth/source/pam/login.go
rename to services/auth/source/pam/source_authenticate.go
index 601a724be9565..ae8ce1fba44f5 100644
--- a/services/auth/source/pam/login.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -15,17 +15,10 @@ import (
 	"github.com/google/uuid"
 )
 
-// __________  _____      _____
-// \______   \/  _  \    /     \
-//  |     ___/  /_\  \  /  \ /  \
-//  |    |  /    |    \/    Y    \
-//  |____|  \____|__  /\____|__  /
-//                  \/         \/
-
-// Login queries if login/password is valid against the PAM,
+// Authenticate queries if login/password is valid against the PAM,
 // and create a local user if success when enabled.
-func Login(user *models.User, login, password string, sourceID int64, cfg *models.PAMConfig) (*models.User, error) {
-	pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
+func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
+	pamLogin, err := pam.Auth(source.ServiceName, login, password)
 	if err != nil {
 		if strings.Contains(err.Error(), "Authentication failure") {
 			return nil, models.ErrUserNotExist{Name: login}
@@ -45,8 +38,8 @@ func Login(user *models.User, login, password string, sourceID int64, cfg *model
 		username = pamLogin[:idx]
 	}
 	if models.ValidateEmail(email) != nil {
-		if cfg.EmailDomain != "" {
-			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
+		if source.EmailDomain != "" {
+			email = fmt.Sprintf("%s@%s", username, source.EmailDomain)
 		} else {
 			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
 		}
@@ -61,7 +54,7 @@ func Login(user *models.User, login, password string, sourceID int64, cfg *model
 		Email:       email,
 		Passwd:      password,
 		LoginType:   models.LoginPAM,
-		LoginSource: sourceID,
+		LoginSource: loginSource.ID,
 		LoginName:   login, // This is what the user typed in
 		IsActive:    true,
 	}
diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go
index a4a7c8ed38a08..8edf4fca15eb6 100644
--- a/services/auth/source/smtp/auth.go
+++ b/services/auth/source/smtp/auth.go
@@ -50,8 +50,8 @@ const (
 var Authenticators = []string{PlainAuthentication, LoginAuthentication}
 
 // Authenticate performs an SMTP authentication.
-func Authenticate(a smtp.Auth, cfg *models.SMTPConfig) error {
-	c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
+func Authenticate(a smtp.Auth, source *Source) error {
+	c, err := smtp.Dial(fmt.Sprintf("%s:%d", source.Host, source.Port))
 	if err != nil {
 		return err
 	}
@@ -61,11 +61,11 @@ func Authenticate(a smtp.Auth, cfg *models.SMTPConfig) error {
 		return err
 	}
 
-	if cfg.TLS {
+	if source.TLS {
 		if ok, _ := c.Extension("STARTTLS"); ok {
 			if err = c.StartTLS(&tls.Config{
-				InsecureSkipVerify: cfg.SkipVerify,
-				ServerName:         cfg.Host,
+				InsecureSkipVerify: source.SkipVerify,
+				ServerName:         source.Host,
 			}); err != nil {
 				return err
 			}
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
new file mode 100644
index 0000000000000..77f2b73cd9d99
--- /dev/null
+++ b/services/auth/source/smtp/source.go
@@ -0,0 +1,58 @@
+// Copyright 2021 The Gitea 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 (
+	"code.gitea.io/gitea/models"
+	jsoniter "github.com/json-iterator/go"
+)
+
+//   _________   __________________________
+//  /   _____/  /     \__    ___/\______   \
+//  \_____  \  /  \ /  \|    |    |     ___/
+//  /        \/    Y    \    |    |    |
+// /_______  /\____|__  /____|    |____|
+//         \/         \/
+
+// Source holds configuration for the SMTP login source.
+type Source struct {
+	Auth           string
+	Host           string
+	Port           int
+	AllowedDomains string `xorm:"TEXT"`
+	TLS            bool
+	SkipVerify     bool
+}
+
+// FromDB fills up an SMTPConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Unmarshal(bs, source)
+}
+
+// ToDB exports an SMTPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Marshal(source)
+}
+
+// IsSkipVerify returns if SkipVerify is set
+func (source *Source) IsSkipVerify() bool {
+	return source.SkipVerify
+}
+
+// HasTLS returns true for SMTP
+func (source *Source) HasTLS() bool {
+	return true
+}
+
+// UseTLS returns if TLS is set
+func (source *Source) UseTLS() bool {
+	return source.TLS
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginSMTP, &Source{})
+}
diff --git a/services/auth/source/smtp/login.go b/services/auth/source/smtp/source_authenticate.go
similarity index 62%
rename from services/auth/source/smtp/login.go
rename to services/auth/source/smtp/source_authenticate.go
index 3248913deb90d..b29525d3f53c4 100644
--- a/services/auth/source/smtp/login.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -14,36 +14,29 @@ import (
 	"code.gitea.io/gitea/modules/util"
 )
 
-//   _________   __________________________
-//  /   _____/  /     \__    ___/\______   \
-//  \_____  \  /  \ /  \|    |    |     ___/
-//  /        \/    Y    \    |    |    |
-// /_______  /\____|__  /____|    |____|
-//         \/         \/
-
-// Login queries if login/password is valid against the SMTP,
-// and create a local user if success when enabled.
-func Login(user *models.User, login, password string, sourceID int64, cfg *models.SMTPConfig) (*models.User, error) {
+// Authenticate queries if the provided login/password is authenticates against the SMTP server
+// Users will be autoregistered as required
+func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
 	// Verify allowed domains.
-	if len(cfg.AllowedDomains) > 0 {
+	if len(source.AllowedDomains) > 0 {
 		idx := strings.Index(login, "@")
 		if idx == -1 {
 			return nil, models.ErrUserNotExist{Name: login}
-		} else if !util.IsStringInSlice(login[idx+1:], strings.Split(cfg.AllowedDomains, ","), true) {
+		} else if !util.IsStringInSlice(login[idx+1:], strings.Split(source.AllowedDomains, ","), true) {
 			return nil, models.ErrUserNotExist{Name: login}
 		}
 	}
 
 	var auth smtp.Auth
-	if cfg.Auth == PlainAuthentication {
-		auth = smtp.PlainAuth("", login, password, cfg.Host)
-	} else if cfg.Auth == LoginAuthentication {
+	if source.Auth == PlainAuthentication {
+		auth = smtp.PlainAuth("", login, password, source.Host)
+	} else if source.Auth == LoginAuthentication {
 		auth = &loginAuthenticator{login, password}
 	} else {
 		return nil, errors.New("Unsupported SMTP auth type")
 	}
 
-	if err := Authenticate(auth, cfg); err != nil {
+	if err := Authenticate(auth, source); err != nil {
 		// Check standard error format first,
 		// then fallback to worse case.
 		tperr, ok := err.(*textproto.Error)
@@ -70,7 +63,7 @@ func Login(user *models.User, login, password string, sourceID int64, cfg *model
 		Email:       login,
 		Passwd:      password,
 		LoginType:   models.LoginSMTP,
-		LoginSource: sourceID,
+		LoginSource: loginSource.ID,
 		LoginName:   login,
 		IsActive:    true,
 	}
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
new file mode 100644
index 0000000000000..fe0ff143c7698
--- /dev/null
+++ b/services/auth/source/sspi/source.go
@@ -0,0 +1,42 @@
+// Copyright 2021 The Gitea 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 sspi
+
+import (
+	"code.gitea.io/gitea/models"
+	jsoniter "github.com/json-iterator/go"
+)
+
+//   _________ ___________________.___
+//  /   _____//   _____/\______   \   |
+//  \_____  \ \_____  \  |     ___/   |
+//  /        \/        \ |    |   |   |
+// /_______  /_______  / |____|   |___|
+//         \/        \/
+
+// Source holds configuration for SSPI single sign-on.
+type Source struct {
+	AutoCreateUsers      bool
+	AutoActivateUsers    bool
+	StripDomainNames     bool
+	SeparatorReplacement string
+	DefaultLanguage      string
+}
+
+// FromDB fills up an SSPIConfig from serialized format.
+func (cfg *Source) FromDB(bs []byte) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Unmarshal(bs, cfg)
+}
+
+// ToDB exports an SSPIConfig to a serialized format.
+func (cfg *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	return json.Marshal(cfg)
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginSSPI, &Source{})
+}
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go
index 01243123081a5..1d31ceaf9e1ce 100644
--- a/services/auth/sspi_windows.go
+++ b/services/auth/sspi_windows.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/auth/source/sspi"
 
 	gouuid "github.com/google/uuid"
 	"github.com/quasoft/websspi"
@@ -146,7 +147,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
 }
 
 // getConfig retrieves the SSPI configuration from login sources
-func (s *SSPI) getConfig() (*models.SSPIConfig, error) {
+func (s *SSPI) getConfig() (*sspi.Source, error) {
 	sources, err := models.ActiveLoginSources(models.LoginSSPI)
 	if err != nil {
 		return nil, err
@@ -157,7 +158,7 @@ func (s *SSPI) getConfig() (*models.SSPIConfig, error) {
 	if len(sources) > 1 {
 		return nil, errors.New("more than one active login source of type SSPI found")
 	}
-	return sources[0].SSPI(), nil
+	return sources[0].Cfg.(*sspi.Source), nil
 }
 
 func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
@@ -177,7 +178,7 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
 
 // newUser creates a new user object for the purpose of automatic registration
 // and populates its name and email with the information present in request headers.
-func (s *SSPI) newUser(username string, cfg *models.SSPIConfig) (*models.User, error) {
+func (s *SSPI) newUser(username string, cfg *sspi.Source) (*models.User, error) {
 	email := gouuid.New().String() + "@localhost.localdomain"
 	user := &models.User{
 		Name:                         username,
@@ -214,7 +215,7 @@ func stripDomainNames(username string) string {
 	return username
 }
 
-func replaceSeparators(username string, cfg *models.SSPIConfig) string {
+func replaceSeparators(username string, cfg *sspi.Source) string {
 	newSep := cfg.SeparatorReplacement
 	username = strings.ReplaceAll(username, "\\", newSep)
 	username = strings.ReplaceAll(username, "/", newSep)
@@ -222,7 +223,7 @@ func replaceSeparators(username string, cfg *models.SSPIConfig) string {
 	return username
 }
 
-func sanitizeUsername(username string, cfg *models.SSPIConfig) string {
+func sanitizeUsername(username string, cfg *sspi.Source) string {
 	if len(username) == 0 {
 		return ""
 	}
diff --git a/services/auth/sync.go b/services/auth/sync.go
index 613c313f43eda..a976270464681 100644
--- a/services/auth/sync.go
+++ b/services/auth/sync.go
@@ -9,7 +9,6 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/services/auth/source/ldap"
 )
 
 // SyncExternalUsers is used to synchronize users with external authorization source
@@ -33,8 +32,8 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 		default:
 		}
 
-		if s.IsLDAP() {
-			err := ldap.Sync(ctx, updateExisting, s)
+		if syncable, ok := s.Cfg.(SynchronizableSource); ok {
+			err := syncable.Sync(ctx, updateExisting, s)
 			if err != nil {
 				return err
 			}

From dd346f17fd4a52655c939120f95ff8de0bd585cc Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 10:43:02 +0100
Subject: [PATCH 08/44] move modules/auth/ldap to services/auth/source/ldap

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/admin_auth_ldap.go                        |  65 ++-
 cmd/admin_auth_ldap_test.go                   | 434 +++++++-----------
 routers/web/admin/auths.go                    |  61 ++-
 .../auth/source}/ldap/README.md               |   0
 .../auth/source/ldap/security_protocol.go     |  27 ++
 services/auth/source/ldap/source.go           |  57 ++-
 .../auth/source/ldap/source_search.go         |  55 ---
 7 files changed, 303 insertions(+), 396 deletions(-)
 rename {modules/auth => services/auth/source}/ldap/README.md (100%)
 create mode 100644 services/auth/source/ldap/security_protocol.go
 rename modules/auth/ldap/ldap.go => services/auth/source/ldap/source_search.go (83%)

diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index aab26082e90b6..6427add8ab42b 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -9,8 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
-	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
+	"code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/urfave/cli"
 )
@@ -181,70 +180,70 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
 }
 
 // parseLdapConfig assigns values on config according to command line flags.
-func parseLdapConfig(c *cli.Context, config *ldapService.Source) error {
+func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
 	if c.IsSet("name") {
-		config.Source.Name = c.String("name")
+		config.Name = c.String("name")
 	}
 	if c.IsSet("host") {
-		config.Source.Host = c.String("host")
+		config.Host = c.String("host")
 	}
 	if c.IsSet("port") {
-		config.Source.Port = c.Int("port")
+		config.Port = c.Int("port")
 	}
 	if c.IsSet("security-protocol") {
 		p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
 		if !ok {
 			return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
 		}
-		config.Source.SecurityProtocol = p
+		config.SecurityProtocol = p
 	}
 	if c.IsSet("skip-tls-verify") {
-		config.Source.SkipVerify = c.Bool("skip-tls-verify")
+		config.SkipVerify = c.Bool("skip-tls-verify")
 	}
 	if c.IsSet("bind-dn") {
-		config.Source.BindDN = c.String("bind-dn")
+		config.BindDN = c.String("bind-dn")
 	}
 	if c.IsSet("user-dn") {
-		config.Source.UserDN = c.String("user-dn")
+		config.UserDN = c.String("user-dn")
 	}
 	if c.IsSet("bind-password") {
-		config.Source.BindPassword = c.String("bind-password")
+		config.BindPassword = c.String("bind-password")
 	}
 	if c.IsSet("user-search-base") {
-		config.Source.UserBase = c.String("user-search-base")
+		config.UserBase = c.String("user-search-base")
 	}
 	if c.IsSet("username-attribute") {
-		config.Source.AttributeUsername = c.String("username-attribute")
+		config.AttributeUsername = c.String("username-attribute")
 	}
 	if c.IsSet("firstname-attribute") {
-		config.Source.AttributeName = c.String("firstname-attribute")
+		config.AttributeName = c.String("firstname-attribute")
 	}
 	if c.IsSet("surname-attribute") {
-		config.Source.AttributeSurname = c.String("surname-attribute")
+		config.AttributeSurname = c.String("surname-attribute")
 	}
 	if c.IsSet("email-attribute") {
-		config.Source.AttributeMail = c.String("email-attribute")
+		config.AttributeMail = c.String("email-attribute")
 	}
 	if c.IsSet("attributes-in-bind") {
-		config.Source.AttributesInBind = c.Bool("attributes-in-bind")
+		config.AttributesInBind = c.Bool("attributes-in-bind")
 	}
 	if c.IsSet("public-ssh-key-attribute") {
-		config.Source.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
+		config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
 	}
 	if c.IsSet("page-size") {
-		config.Source.SearchPageSize = uint32(c.Uint("page-size"))
+		config.SearchPageSize = uint32(c.Uint("page-size"))
 	}
 	if c.IsSet("user-filter") {
-		config.Source.Filter = c.String("user-filter")
+		config.Filter = c.String("user-filter")
 	}
 	if c.IsSet("admin-filter") {
-		config.Source.AdminFilter = c.String("admin-filter")
+		config.AdminFilter = c.String("admin-filter")
 	}
 	if c.IsSet("restricted-filter") {
-		config.Source.RestrictedFilter = c.String("restricted-filter")
+		config.RestrictedFilter = c.String("restricted-filter")
 	}
 	if c.IsSet("allow-deactivate-all") {
-		config.Source.AllowDeactivateAll = c.Bool("allow-deactivate-all")
+		config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
 	}
 	return nil
 }
@@ -292,15 +291,13 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 	loginSource := &models.LoginSource{
 		Type:      models.LoginLDAP,
 		IsActived: true, // active by default
-		Cfg: &ldapService.Source{
-			Source: &ldap.Source{
-				Enabled: true, // always true
-			},
+		Cfg: &ldap.Source{
+			Enabled: true, // always true
 		},
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -319,7 +316,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -339,15 +336,13 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 	loginSource := &models.LoginSource{
 		Type:      models.LoginDLDAP,
 		IsActived: true, // active by default
-		Cfg: &ldapService.Source{
-			Source: &ldap.Source{
-				Enabled: true, // always true
-			},
+		Cfg: &ldap.Source{
+			Enabled: true, // always true
 		},
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -366,7 +361,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.Cfg.(*ldapService.Source)); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index d051feee5bcd5..bcf4325f0601b 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -8,8 +8,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
-	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
+	"code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/urfave/cli"
@@ -57,28 +56,26 @@ func TestAddLdapBindDn(t *testing.T) {
 				Name:          "ldap (via Bind DN) source full",
 				IsActived:     false,
 				IsSyncEnabled: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:                  "ldap (via Bind DN) source full",
-						Host:                  "ldap-bind-server full",
-						Port:                  9876,
-						SecurityProtocol:      ldap.SecurityProtocol(1),
-						SkipVerify:            true,
-						BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
-						BindPassword:          "secret-bind-full",
-						UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
-						AttributeUsername:     "uid-bind full",
-						AttributeName:         "givenName-bind full",
-						AttributeSurname:      "sn-bind full",
-						AttributeMail:         "mail-bind full",
-						AttributesInBind:      true,
-						AttributeSSHPublicKey: "publickey-bind full",
-						SearchPageSize:        99,
-						Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
-						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
-						RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
-						Enabled:               true,
-					},
+				Cfg: &ldap.Source{
+					Name:                  "ldap (via Bind DN) source full",
+					Host:                  "ldap-bind-server full",
+					Port:                  9876,
+					SecurityProtocol:      ldap.SecurityProtocol(1),
+					SkipVerify:            true,
+					BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
+					BindPassword:          "secret-bind-full",
+					UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
+					AttributeUsername:     "uid-bind full",
+					AttributeName:         "givenName-bind full",
+					AttributeSurname:      "sn-bind full",
+					AttributeMail:         "mail-bind full",
+					AttributesInBind:      true,
+					AttributeSSHPublicKey: "publickey-bind full",
+					SearchPageSize:        99,
+					Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
+					Enabled:               true,
 				},
 			},
 		},
@@ -98,17 +95,15 @@ func TestAddLdapBindDn(t *testing.T) {
 				Type:      models.LoginLDAP,
 				Name:      "ldap (via Bind DN) source min",
 				IsActived: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:             "ldap (via Bind DN) source min",
-						Host:             "ldap-bind-server min",
-						Port:             1234,
-						SecurityProtocol: ldap.SecurityProtocol(0),
-						UserBase:         "ou=Users,dc=min-domain-bind,dc=org",
-						AttributeMail:    "mail-bind min",
-						Filter:           "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)",
-						Enabled:          true,
-					},
+				Cfg: &ldap.Source{
+					Name:             "ldap (via Bind DN) source min",
+					Host:             "ldap-bind-server min",
+					Port:             1234,
+					SecurityProtocol: ldap.SecurityProtocol(0),
+					UserBase:         "ou=Users,dc=min-domain-bind,dc=org",
+					AttributeMail:    "mail-bind min",
+					Filter:           "(memberOf=cn=user-group,ou=example,dc=min-domain-bind,dc=org)",
+					Enabled:          true,
 				},
 			},
 		},
@@ -280,25 +275,23 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source full",
 				IsActived: false,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:                  "ldap (simple auth) source full",
-						Host:                  "ldap-simple-server full",
-						Port:                  987,
-						SecurityProtocol:      ldap.SecurityProtocol(2),
-						SkipVerify:            true,
-						UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
-						UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
-						AttributeUsername:     "uid-simple full",
-						AttributeName:         "givenName-simple full",
-						AttributeSurname:      "sn-simple full",
-						AttributeMail:         "mail-simple full",
-						AttributeSSHPublicKey: "publickey-simple full",
-						Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
-						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
-						RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
-						Enabled:               true,
-					},
+				Cfg: &ldap.Source{
+					Name:                  "ldap (simple auth) source full",
+					Host:                  "ldap-simple-server full",
+					Port:                  987,
+					SecurityProtocol:      ldap.SecurityProtocol(2),
+					SkipVerify:            true,
+					UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+					UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
+					AttributeUsername:     "uid-simple full",
+					AttributeName:         "givenName-simple full",
+					AttributeSurname:      "sn-simple full",
+					AttributeMail:         "mail-simple full",
+					AttributeSSHPublicKey: "publickey-simple full",
+					Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
+					Enabled:               true,
 				},
 			},
 		},
@@ -318,17 +311,15 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source min",
 				IsActived: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:             "ldap (simple auth) source min",
-						Host:             "ldap-simple-server min",
-						Port:             123,
-						SecurityProtocol: ldap.SecurityProtocol(0),
-						UserDN:           "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
-						AttributeMail:    "mail-simple min",
-						Filter:           "(&(objectClass=posixAccount)(min-simple-cn=%s))",
-						Enabled:          true,
-					},
+				Cfg: &ldap.Source{
+					Name:             "ldap (simple auth) source min",
+					Host:             "ldap-simple-server min",
+					Port:             123,
+					SecurityProtocol: ldap.SecurityProtocol(0),
+					UserDN:           "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
+					AttributeMail:    "mail-simple min",
+					Filter:           "(&(objectClass=posixAccount)(min-simple-cn=%s))",
+					Enabled:          true,
 				},
 			},
 		},
@@ -519,10 +510,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Enabled: true,
-					},
+				Cfg: &ldap.Source{
+					Enabled: true,
 				},
 			},
 			loginSource: &models.LoginSource{
@@ -530,28 +519,26 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				Name:          "ldap (via Bind DN) source full",
 				IsActived:     false,
 				IsSyncEnabled: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:                  "ldap (via Bind DN) source full",
-						Host:                  "ldap-bind-server full",
-						Port:                  9876,
-						SecurityProtocol:      ldap.SecurityProtocol(1),
-						SkipVerify:            true,
-						BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
-						BindPassword:          "secret-bind-full",
-						UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
-						AttributeUsername:     "uid-bind full",
-						AttributeName:         "givenName-bind full",
-						AttributeSurname:      "sn-bind full",
-						AttributeMail:         "mail-bind full",
-						AttributesInBind:      false,
-						AttributeSSHPublicKey: "publickey-bind full",
-						SearchPageSize:        99,
-						Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
-						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
-						RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
-						Enabled:               true,
-					},
+				Cfg: &ldap.Source{
+					Name:                  "ldap (via Bind DN) source full",
+					Host:                  "ldap-bind-server full",
+					Port:                  9876,
+					SecurityProtocol:      ldap.SecurityProtocol(1),
+					SkipVerify:            true,
+					BindDN:                "cn=readonly,dc=full-domain-bind,dc=org",
+					BindPassword:          "secret-bind-full",
+					UserBase:              "ou=Users,dc=full-domain-bind,dc=org",
+					AttributeUsername:     "uid-bind full",
+					AttributeName:         "givenName-bind full",
+					AttributeSurname:      "sn-bind full",
+					AttributeMail:         "mail-bind full",
+					AttributesInBind:      false,
+					AttributeSSHPublicKey: "publickey-bind full",
+					SearchPageSize:        99,
+					Filter:                "(memberOf=cn=user-group,ou=example,dc=full-domain-bind,dc=org)",
+					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
+					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
+					Enabled:               true,
 				},
 			},
 		},
@@ -563,9 +550,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 		},
 		// case 2
@@ -578,10 +563,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
 				Name: "ldap (via Bind DN) source",
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name: "ldap (via Bind DN) source",
-					},
+				Cfg: &ldap.Source{
+					Name: "ldap (via Bind DN) source",
 				},
 			},
 		},
@@ -595,16 +578,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:       &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
 				Type:      models.LoginLDAP,
 				IsActived: false,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:       &ldap.Source{},
 			},
 		},
 		// case 4
@@ -616,10 +595,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						SecurityProtocol: ldap.SecurityProtocol(1),
-					},
+				Cfg: &ldap.Source{
+					SecurityProtocol: ldap.SecurityProtocol(1),
 				},
 			},
 		},
@@ -632,10 +609,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						SkipVerify: true,
-					},
+				Cfg: &ldap.Source{
+					SkipVerify: true,
 				},
 			},
 		},
@@ -648,10 +623,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Host: "ldap-server",
-					},
+				Cfg: &ldap.Source{
+					Host: "ldap-server",
 				},
 			},
 		},
@@ -664,10 +637,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Port: 389,
-					},
+				Cfg: &ldap.Source{
+					Port: 389,
 				},
 			},
 		},
@@ -680,10 +651,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						UserBase: "ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserBase: "ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -696,10 +665,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
-					},
+				Cfg: &ldap.Source{
+					Filter: "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
 				},
 			},
 		},
@@ -712,10 +679,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
-					},
+				Cfg: &ldap.Source{
+					AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 				},
 			},
 		},
@@ -728,10 +693,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeUsername: "uid",
-					},
+				Cfg: &ldap.Source{
+					AttributeUsername: "uid",
 				},
 			},
 		},
@@ -744,10 +707,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeName: "givenName",
-					},
+				Cfg: &ldap.Source{
+					AttributeName: "givenName",
 				},
 			},
 		},
@@ -760,10 +721,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeSurname: "sn",
-					},
+				Cfg: &ldap.Source{
+					AttributeSurname: "sn",
 				},
 			},
 		},
@@ -776,10 +735,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeMail: "mail",
-					},
+				Cfg: &ldap.Source{
+					AttributeMail: "mail",
 				},
 			},
 		},
@@ -792,10 +749,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributesInBind: true,
-					},
+				Cfg: &ldap.Source{
+					AttributesInBind: true,
 				},
 			},
 		},
@@ -808,10 +763,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeSSHPublicKey: "publickey",
-					},
+				Cfg: &ldap.Source{
+					AttributeSSHPublicKey: "publickey",
 				},
 			},
 		},
@@ -824,10 +777,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						BindDN: "cn=readonly,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					BindDN: "cn=readonly,dc=domain,dc=org",
 				},
 			},
 		},
@@ -840,10 +791,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						BindPassword: "secret",
-					},
+				Cfg: &ldap.Source{
+					BindPassword: "secret",
 				},
 			},
 		},
@@ -857,9 +806,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				IsSyncEnabled: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:           &ldap.Source{},
 			},
 		},
 		// case 20
@@ -871,10 +818,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						SearchPageSize: 12,
-					},
+				Cfg: &ldap.Source{
+					SearchPageSize: 12,
 				},
 			},
 		},
@@ -902,9 +847,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginOAuth2,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 			errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 		},
@@ -934,9 +877,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginLDAP,
-					Cfg: &ldapService.Source{
-						Source: &ldap.Source{},
-					},
+					Cfg:  &ldap.Source{},
 				}, nil
 			},
 		}
@@ -998,24 +939,22 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				Type:      models.LoginDLDAP,
 				Name:      "ldap (simple auth) source full",
 				IsActived: false,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name:                  "ldap (simple auth) source full",
-						Host:                  "ldap-simple-server full",
-						Port:                  987,
-						SecurityProtocol:      ldap.SecurityProtocol(2),
-						SkipVerify:            true,
-						UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
-						UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
-						AttributeUsername:     "uid-simple full",
-						AttributeName:         "givenName-simple full",
-						AttributeSurname:      "sn-simple full",
-						AttributeMail:         "mail-simple full",
-						AttributeSSHPublicKey: "publickey-simple full",
-						Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
-						AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
-						RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
-					},
+				Cfg: &ldap.Source{
+					Name:                  "ldap (simple auth) source full",
+					Host:                  "ldap-simple-server full",
+					Port:                  987,
+					SecurityProtocol:      ldap.SecurityProtocol(2),
+					SkipVerify:            true,
+					UserDN:                "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
+					UserBase:              "ou=Users,dc=full-domain-simple,dc=org",
+					AttributeUsername:     "uid-simple full",
+					AttributeName:         "givenName-simple full",
+					AttributeSurname:      "sn-simple full",
+					AttributeMail:         "mail-simple full",
+					AttributeSSHPublicKey: "publickey-simple full",
+					Filter:                "(&(objectClass=posixAccount)(full-simple-cn=%s))",
+					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-simple,dc=org)",
+					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-simple,dc=org)",
 				},
 			},
 		},
@@ -1027,9 +966,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 		},
 		// case 2
@@ -1042,10 +979,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
 				Name: "ldap (simple auth) source",
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Name: "ldap (simple auth) source",
-					},
+				Cfg: &ldap.Source{
+					Name: "ldap (simple auth) source",
 				},
 			},
 		},
@@ -1059,16 +994,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			existingLoginSource: &models.LoginSource{
 				Type:      models.LoginDLDAP,
 				IsActived: true,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:       &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
 				Type:      models.LoginDLDAP,
 				IsActived: false,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:       &ldap.Source{},
 			},
 		},
 		// case 4
@@ -1080,10 +1011,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						SecurityProtocol: ldap.SecurityProtocol(2),
-					},
+				Cfg: &ldap.Source{
+					SecurityProtocol: ldap.SecurityProtocol(2),
 				},
 			},
 		},
@@ -1096,10 +1025,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						SkipVerify: true,
-					},
+				Cfg: &ldap.Source{
+					SkipVerify: true,
 				},
 			},
 		},
@@ -1112,10 +1039,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Host: "ldap-server",
-					},
+				Cfg: &ldap.Source{
+					Host: "ldap-server",
 				},
 			},
 		},
@@ -1128,10 +1053,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Port: 987,
-					},
+				Cfg: &ldap.Source{
+					Port: 987,
 				},
 			},
 		},
@@ -1144,10 +1067,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						UserBase: "ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserBase: "ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -1160,10 +1081,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						Filter: "(&(objectClass=posixAccount)(cn=%s))",
-					},
+				Cfg: &ldap.Source{
+					Filter: "(&(objectClass=posixAccount)(cn=%s))",
 				},
 			},
 		},
@@ -1176,10 +1095,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
-					},
+				Cfg: &ldap.Source{
+					AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=domain,dc=org)",
 				},
 			},
 		},
@@ -1192,10 +1109,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeUsername: "uid",
-					},
+				Cfg: &ldap.Source{
+					AttributeUsername: "uid",
 				},
 			},
 		},
@@ -1208,10 +1123,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeName: "givenName",
-					},
+				Cfg: &ldap.Source{
+					AttributeName: "givenName",
 				},
 			},
 		},
@@ -1224,10 +1137,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeSurname: "sn",
-					},
+				Cfg: &ldap.Source{
+					AttributeSurname: "sn",
 				},
 			},
 		},
@@ -1240,10 +1151,9 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeMail: "mail",
-					},
+				Cfg: &ldap.Source{
+
+					AttributeMail: "mail",
 				},
 			},
 		},
@@ -1256,10 +1166,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						AttributeSSHPublicKey: "publickey",
-					},
+				Cfg: &ldap.Source{
+					AttributeSSHPublicKey: "publickey",
 				},
 			},
 		},
@@ -1272,10 +1180,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{
-						UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -1303,9 +1209,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginPAM,
-				Cfg: &ldapService.Source{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 			errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 		},
@@ -1335,9 +1239,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginDLDAP,
-					Cfg: &ldapService.Source{
-						Source: &ldap.Source{},
-					},
+					Cfg:  &ldap.Source{},
 				}, nil
 			},
 		}
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index dd3036213ac6d..a8d3f7cd8801d 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -11,7 +11,6 @@ import (
 	"regexp"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
 	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/auth/pam"
 	"code.gitea.io/gitea/modules/base"
@@ -20,7 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
-	ldapService "code.gitea.io/gitea/services/auth/source/ldap"
+	"code.gitea.io/gitea/services/auth/source/ldap"
 	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
 	pamService "code.gitea.io/gitea/services/auth/source/pam"
 	"code.gitea.io/gitea/services/auth/source/smtp"
@@ -118,40 +117,38 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplAuthNew)
 }
 
-func parseLDAPConfig(form forms.AuthenticationForm) *ldapService.Source {
+func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 	var pageSize uint32
 	if form.UsePagedSearch {
 		pageSize = uint32(form.SearchPageSize)
 	}
-	return &ldapService.Source{
-		Source: &ldap.Source{
-			Name:                  form.Name,
-			Host:                  form.Host,
-			Port:                  form.Port,
-			SecurityProtocol:      ldap.SecurityProtocol(form.SecurityProtocol),
-			SkipVerify:            form.SkipVerify,
-			BindDN:                form.BindDN,
-			UserDN:                form.UserDN,
-			BindPassword:          form.BindPassword,
-			UserBase:              form.UserBase,
-			AttributeUsername:     form.AttributeUsername,
-			AttributeName:         form.AttributeName,
-			AttributeSurname:      form.AttributeSurname,
-			AttributeMail:         form.AttributeMail,
-			AttributesInBind:      form.AttributesInBind,
-			AttributeSSHPublicKey: form.AttributeSSHPublicKey,
-			SearchPageSize:        pageSize,
-			Filter:                form.Filter,
-			GroupsEnabled:         form.GroupsEnabled,
-			GroupDN:               form.GroupDN,
-			GroupFilter:           form.GroupFilter,
-			GroupMemberUID:        form.GroupMemberUID,
-			UserUID:               form.UserUID,
-			AdminFilter:           form.AdminFilter,
-			RestrictedFilter:      form.RestrictedFilter,
-			AllowDeactivateAll:    form.AllowDeactivateAll,
-			Enabled:               true,
-		},
+	return &ldap.Source{
+		Name:                  form.Name,
+		Host:                  form.Host,
+		Port:                  form.Port,
+		SecurityProtocol:      ldap.SecurityProtocol(form.SecurityProtocol),
+		SkipVerify:            form.SkipVerify,
+		BindDN:                form.BindDN,
+		UserDN:                form.UserDN,
+		BindPassword:          form.BindPassword,
+		UserBase:              form.UserBase,
+		AttributeUsername:     form.AttributeUsername,
+		AttributeName:         form.AttributeName,
+		AttributeSurname:      form.AttributeSurname,
+		AttributeMail:         form.AttributeMail,
+		AttributesInBind:      form.AttributesInBind,
+		AttributeSSHPublicKey: form.AttributeSSHPublicKey,
+		SearchPageSize:        pageSize,
+		Filter:                form.Filter,
+		GroupsEnabled:         form.GroupsEnabled,
+		GroupDN:               form.GroupDN,
+		GroupFilter:           form.GroupFilter,
+		GroupMemberUID:        form.GroupMemberUID,
+		UserUID:               form.UserUID,
+		AdminFilter:           form.AdminFilter,
+		RestrictedFilter:      form.RestrictedFilter,
+		AllowDeactivateAll:    form.AllowDeactivateAll,
+		Enabled:               true,
 	}
 }
 
diff --git a/modules/auth/ldap/README.md b/services/auth/source/ldap/README.md
similarity index 100%
rename from modules/auth/ldap/README.md
rename to services/auth/source/ldap/README.md
diff --git a/services/auth/source/ldap/security_protocol.go b/services/auth/source/ldap/security_protocol.go
new file mode 100644
index 0000000000000..47c9d30e5cfe0
--- /dev/null
+++ b/services/auth/source/ldap/security_protocol.go
@@ -0,0 +1,27 @@
+// Copyright 2021 The Gitea 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
+
+// SecurityProtocol protocol type
+type SecurityProtocol int
+
+// Note: new type must be added at the end of list to maintain compatibility.
+const (
+	SecurityProtocolUnencrypted SecurityProtocol = iota
+	SecurityProtocolLDAPS
+	SecurityProtocolStartTLS
+)
+
+// String returns the name of the SecurityProtocol
+func (s SecurityProtocol) String() string {
+	return SecurityProtocolNames[s]
+}
+
+// SecurityProtocolNames contains the name of SecurityProtocol values.
+var SecurityProtocolNames = map[SecurityProtocol]string{
+	SecurityProtocolUnencrypted: "Unencrypted",
+	SecurityProtocolLDAPS:       "LDAPS",
+	SecurityProtocolStartTLS:    "StartTLS",
+}
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index bf0c69154e41f..fc6a62708fe56 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -8,7 +8,6 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
 	jsoniter "github.com/json-iterator/go"
@@ -21,15 +20,54 @@ import (
 // |_______ \/_______  /\____|__  /____|
 //         \/        \/         \/
 
-// Source holds configuration for LDAP login source.
+// 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
+
+// Source Basic LDAP authentication service
 type Source struct {
-	*ldap.Source
+	Name                  string // canonical name (ie. corporate.ad)
+	Host                  string // LDAP host
+	Port                  int    // port number
+	SecurityProtocol      SecurityProtocol
+	SkipVerify            bool
+	BindDN                string // DN to bind with
+	BindPasswordEncrypt   string // Encrypted Bind BN password
+	BindPassword          string // Bind DN password
+	UserBase              string // Base search path for users
+	UserDN                string // Template for the DN of the user for simple auth
+	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)
+	AttributeSSHPublicKey string // LDAP SSH Public Key attribute
+	SearchPageSize        uint32 // Search with paging page size
+	Filter                string // Query filter to validate entry
+	AdminFilter           string // Query filter to check if user is admin
+	RestrictedFilter      string // Query filter to check if user is restricted
+	Enabled               bool   // if this source is disabled
+	AllowDeactivateAll    bool   // Allow an empty search response to deactivate all users from this source
+	GroupsEnabled         bool   // if the group checking is enabled
+	GroupDN               string // Group Search Base
+	GroupFilter           string // Group Name Filter
+	GroupMemberUID        string // Group Attribute containing array of UserUID
+	UserUID               string // User Attribute listed in Group
+}
+
+// wrappedSource wraps the source to ensure that the FromDB/ToDB results are the same as previously
+type wrappedSource struct {
+	Source *Source
 }
 
 // FromDB fills up a LDAPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
 	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	err := json.Unmarshal(bs, &source)
+
+	wrapped := &wrappedSource{
+		Source: source,
+	}
+
+	err := json.Unmarshal(bs, &wrapped)
 	if err != nil {
 		return err
 	}
@@ -49,13 +87,16 @@ func (source *Source) ToDB() ([]byte, error) {
 	}
 	source.BindPassword = ""
 	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(source)
+	wrapped := &wrappedSource{
+		Source: source,
+	}
+	return json.Marshal(wrapped)
 }
 
 // SecurityProtocolName returns the name of configured security
 // protocol.
 func (source *Source) SecurityProtocolName() string {
-	return ldap.SecurityProtocolNames[source.SecurityProtocol]
+	return SecurityProtocolNames[source.SecurityProtocol]
 }
 
 // IsSkipVerify returns if SkipVerify is set
@@ -65,12 +106,12 @@ func (source *Source) IsSkipVerify() bool {
 
 // HasTLS returns if HasTLS
 func (source *Source) HasTLS() bool {
-	return source.SecurityProtocol > ldap.SecurityProtocolUnencrypted
+	return source.SecurityProtocol > SecurityProtocolUnencrypted
 }
 
 // UseTLS returns if UseTLS
 func (source *Source) UseTLS() bool {
-	return source.SecurityProtocol != ldap.SecurityProtocolUnencrypted
+	return source.SecurityProtocol != SecurityProtocolUnencrypted
 }
 
 // ProvidesSSHKeys returns if this source provides SSH Keys
diff --git a/modules/auth/ldap/ldap.go b/services/auth/source/ldap/source_search.go
similarity index 83%
rename from modules/auth/ldap/ldap.go
rename to services/auth/source/ldap/source_search.go
index 321fbf805891a..e99fc67901afd 100644
--- a/modules/auth/ldap/ldap.go
+++ b/services/auth/source/ldap/source_search.go
@@ -3,8 +3,6 @@
 // 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
 
 import (
@@ -17,59 +15,6 @@ import (
 	"github.com/go-ldap/ldap/v3"
 )
 
-// SecurityProtocol protocol type
-type SecurityProtocol int
-
-// Note: new type must be added at the end of list to maintain compatibility.
-const (
-	SecurityProtocolUnencrypted SecurityProtocol = iota
-	SecurityProtocolLDAPS
-	SecurityProtocolStartTLS
-)
-
-// String returns the name of the SecurityProtocol
-func (s SecurityProtocol) String() string {
-	return SecurityProtocolNames[s]
-}
-
-// SecurityProtocolNames contains the name of SecurityProtocol values.
-var SecurityProtocolNames = map[SecurityProtocol]string{
-	SecurityProtocolUnencrypted: "Unencrypted",
-	SecurityProtocolLDAPS:       "LDAPS",
-	SecurityProtocolStartTLS:    "StartTLS",
-}
-
-// Source Basic LDAP authentication service
-type Source struct {
-	Name                  string // canonical name (ie. corporate.ad)
-	Host                  string // LDAP host
-	Port                  int    // port number
-	SecurityProtocol      SecurityProtocol
-	SkipVerify            bool
-	BindDN                string // DN to bind with
-	BindPasswordEncrypt   string // Encrypted Bind BN password
-	BindPassword          string // Bind DN password
-	UserBase              string // Base search path for users
-	UserDN                string // Template for the DN of the user for simple auth
-	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)
-	AttributeSSHPublicKey string // LDAP SSH Public Key attribute
-	SearchPageSize        uint32 // Search with paging page size
-	Filter                string // Query filter to validate entry
-	AdminFilter           string // Query filter to check if user is admin
-	RestrictedFilter      string // Query filter to check if user is restricted
-	Enabled               bool   // if this source is disabled
-	AllowDeactivateAll    bool   // Allow an empty search response to deactivate all users from this source
-	GroupsEnabled         bool   // if the group checking is enabled
-	GroupDN               string // Group Search Base
-	GroupFilter           string // Group Name Filter
-	GroupMemberUID        string // Group Attribute containing array of UserUID
-	UserUID               string // User Attribute listed in Group
-}
-
 // SearchResult : user data
 type SearchResult struct {
 	Username     string   // Username

From 70f542c462ea390e65a131a5b0e65a6f94afd6a2 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 11:02:22 +0100
Subject: [PATCH 09/44] fix reflection

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/models/login_source.go b/models/login_source.go
index b7b734b4b72e1..9ca6709661c3a 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -116,7 +116,7 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 		typ := LoginType(Cell2Int64(val))
 		exemplar, ok := registeredLoginConfigs[typ]
 		if ok {
-			source.Cfg = reflect.New(reflect.TypeOf(exemplar)).Interface().(convert.Conversion)
+			source.Cfg = reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(convert.Conversion)
 			return
 		}
 	}

From 521f1831bfb3404e3c4b1fa812de9fadbf533247 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 11:15:41 +0100
Subject: [PATCH 10/44] handle non-pointer sources

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/models/login_source.go b/models/login_source.go
index 9ca6709661c3a..e7e45b4e99bed 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -115,10 +115,18 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	if colName == "type" {
 		typ := LoginType(Cell2Int64(val))
 		exemplar, ok := registeredLoginConfigs[typ]
-		if ok {
+		if !ok {
+			return
+		}
+
+		if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
+			// Pointer:
 			source.Cfg = reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(convert.Conversion)
 			return
 		}
+
+		// Not pointer:
+		source.Cfg = reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(convert.Conversion)
 	}
 }
 

From 202c971d6e088ecb860ccb5df2f65a28f764fbfa Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 11:16:59 +0100
Subject: [PATCH 11/44] fix readme file

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/source/ldap/README.md | 77 ++++++++++++++---------------
 1 file changed, 38 insertions(+), 39 deletions(-)

diff --git a/services/auth/source/ldap/README.md b/services/auth/source/ldap/README.md
index 76841f44aefd4..3a839fa3142a9 100644
--- a/services/auth/source/ldap/README.md
+++ b/services/auth/source/ldap/README.md
@@ -1,5 +1,4 @@
-Gitea LDAP Authentication Module
-===============================
+# Gitea LDAP Authentication Module
 
 ## About
 
@@ -30,94 +29,94 @@ section in the admin panel. Both the LDAP via BindDN and the simple auth LDAP
 share the following fields:
 
 * Authorization Name **(required)**
-    * A name to assign to the new method of authorization.
+  * A name to assign to the new method of authorization.
 
 * Host **(required)**
-    * The address where the LDAP server can be reached.
-    * Example: mydomain.com
+  * The address where the LDAP server can be reached.
+  * Example: mydomain.com
 
 * Port **(required)**
-    * The port to use when connecting to the server.
-    * Example: 636
+  * The port to use when connecting to the server.
+  * Example: 636
 
 * Enable TLS Encryption (optional)
-    * Whether to use TLS when connecting to the LDAP server.
+  * Whether to use TLS when connecting to the LDAP server.
 
 * Admin Filter (optional)
-    * An LDAP filter specifying if a user should be given administrator
+  * An LDAP filter specifying if a user should be given administrator
       privileges. If a user accounts passes the filter, the user will be
       privileged as an administrator.
-    * Example: (objectClass=adminAccount)
+  * Example: (objectClass=adminAccount)
 
 * First name attribute (optional)
-    * The attribute of the user's LDAP record containing the user's first name.
+  * The attribute of the user's LDAP record containing the user's first name.
       This will be used to populate their account information.
-    * Example: givenName
+  * Example: givenName
 
 * Surname attribute (optional)
-    * The attribute of the user's LDAP record containing the user's surname This
+  * The attribute of the user's LDAP record containing the user's surname This
       will be used to populate their account information.
-    * Example: sn
+  * Example: sn
 
 * E-mail attribute **(required)**
-    * The attribute of the user's LDAP record containing the user's email
+  * The attribute of the user's LDAP record containing the user's email
       address. This will be used to populate their account information.
-    * Example: mail
+  * Example: mail
 
 **LDAP via BindDN** adds the following fields:
 
 * Bind DN (optional)
-    * The DN to bind to the LDAP server with when searching for the user. This
+  * The DN to bind to the LDAP server with when searching for the user. This
       may be left blank to perform an anonymous search.
-    * Example: cn=Search,dc=mydomain,dc=com
+  * Example: cn=Search,dc=mydomain,dc=com
 
 * Bind Password (optional)
-    * The password for the Bind DN specified above, if any. _Note: The password
+  * The password for the Bind DN specified above, if any. _Note: The password
       is stored in plaintext at the server. As such, ensure that your Bind DN
       has as few privileges as possible._
 
 * User Search Base **(required)**
-    * The LDAP base at which user accounts will be searched for.
-    * Example: ou=Users,dc=mydomain,dc=com
+  * The LDAP base at which user accounts will be searched for.
+  * Example: ou=Users,dc=mydomain,dc=com
 
 * User Filter **(required)**
-    * An LDAP filter declaring how to find the user record that is attempting to
+  * An LDAP filter declaring how to find the user record that is attempting to
       authenticate. The '%s' matching parameter will be substituted with the
       user's username.
-    * Example: (&(objectClass=posixAccount)(uid=%s))
+  * Example: (&(objectClass=posixAccount)(uid=%s))
 
 **LDAP using simple auth** adds the following fields:
 
 * User DN **(required)**
-    * A template to use as the user's DN. The `%s` matching parameter will be
+  * A template to use as the user's DN. The `%s` matching parameter will be
       substituted with the user's username.
-    * Example: cn=%s,ou=Users,dc=mydomain,dc=com
-    * Example: uid=%s,ou=Users,dc=mydomain,dc=com
+  * Example: cn=%s,ou=Users,dc=mydomain,dc=com
+  * Example: uid=%s,ou=Users,dc=mydomain,dc=com
 
 * User Search Base (optional)
-    * The LDAP base at which user accounts will be searched for.
-    * Example: ou=Users,dc=mydomain,dc=com
+  * The LDAP base at which user accounts will be searched for.
+  * Example: ou=Users,dc=mydomain,dc=com
 
 * User Filter **(required)**
-    * An LDAP filter declaring when a user should be allowed to log in. The `%s`
+  * An LDAP filter declaring when a user should be allowed to log in. The `%s`
       matching parameter will be substituted with the user's username.
-    * Example: (&(objectClass=posixAccount)(cn=%s))
-    * Example: (&(objectClass=posixAccount)(uid=%s))
+  * Example: (&(objectClass=posixAccount)(cn=%s))
+  * Example: (&(objectClass=posixAccount)(uid=%s))
 
 **Verify group membership in LDAP** uses the following fields:
 
 * Group Search Base (optional)
-    * The LDAP DN used for groups.
-    * Example: ou=group,dc=mydomain,dc=com
+  * The LDAP DN used for groups.
+  * Example: ou=group,dc=mydomain,dc=com
 
 * Group Name Filter (optional)
-    * An LDAP filter declaring how to find valid groups in the above DN.
-    * Example: (|(cn=gitea_users)(cn=admins))
+  * An LDAP filter declaring how to find valid groups in the above DN.
+  * Example: (|(cn=gitea_users)(cn=admins))
 
 * User Attribute in Group (optional)
-    * Which user LDAP attribute is listed in the group.
-    * Example: uid
+  * Which user LDAP attribute is listed in the group.
+  * Example: uid
 
 * Group Attribute for User (optional)
-    * Which group LDAP attribute contains an array above user attribute names.
-    * Example: memberUid
+  * Which group LDAP attribute contains an array above user attribute names.
+  * Example: memberUid

From 2dd721136cb7e856d7858badf0d605f03073f0e3 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 11:36:13 +0100
Subject: [PATCH 12/44] unregister

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go                         | 8 +++++---
 services/auth/source/oauth2/source_register.go | 7 ++++++-
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/models/login_source.go b/models/login_source.go
index e7e45b4e99bed..fa5b23fbedc3d 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -9,7 +9,6 @@ import (
 	"reflect"
 	"strconv"
 
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 
@@ -75,6 +74,7 @@ type SSHKeyProvider interface {
 // RegisterableSource configurations provide RegisterSource which needs to be run on creation
 type RegisterableSource interface {
 	RegisterSource(*LoginSource) error
+	UnregisterSource(*LoginSource) error
 }
 
 // RegisterLoginTypeConfig register a config for a provided type
@@ -330,8 +330,10 @@ func DeleteSource(source *LoginSource) error {
 		return ErrLoginSourceInUse{source.ID}
 	}
 
-	if source.IsOAuth2() {
-		oauth2.RemoveProvider(source.Name)
+	if registerableSource, ok := source.Cfg.(RegisterableSource); ok {
+		if err := registerableSource.UnregisterSource(source); err != nil {
+			return err
+		}
 	}
 
 	_, err = x.ID(source.ID).Delete(new(LoginSource))
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
index cc901c2f5de34..538cdfd6c23b1 100644
--- a/services/auth/source/oauth2/source_register.go
+++ b/services/auth/source/oauth2/source_register.go
@@ -11,11 +11,16 @@ import (
 
 // RegisterSource causes an OAuth2 configuration to be registered
 func (source *Source) RegisterSource(loginSource *models.LoginSource) error {
-
 	err := oauth2.RegisterProvider(loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
 	return wrapOpenIDConnectInitializeError(err, loginSource.Name, source)
 }
 
+// UnregisterSource causes an OAuth2 configuration to be unregistered
+func (source *Source) UnregisterSource(loginSource *models.LoginSource) error {
+	oauth2.RemoveProvider(loginSource.Name)
+	return nil
+}
+
 // wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
 // inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
 func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *Source) error {

From 2a44869d9e1845389e756bab93cb1c69f7bf8f89 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 16:18:55 +0100
Subject: [PATCH 13/44] Remove modules/auth/oauth2 and begin clean up of oauth2

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/admin.go                                  |   9 +-
 models/oauth2.go                              | 115 -------
 models/oauth2_application.go                  |  77 -----
 models/store.go                               |  16 +
 modules/auth/oauth2/oauth2.go                 | 299 ------------------
 routers/init.go                               |   3 +-
 routers/web/admin/auths.go                    |  27 +-
 routers/web/user/auth.go                      |  10 +-
 routers/web/user/oauth.go                     |  14 +-
 routers/web/user/setting/security.go          |   2 +-
 services/auth/oauth2.go                       |   5 +-
 services/auth/source/ldap/source.go           |   1 +
 services/auth/source/oauth2/init.go           |  83 +++++
 .../auth/source}/oauth2/jwtsigningkey.go      |   0
 services/auth/source/oauth2/providers.go      | 225 ++++++++++++-
 services/auth/source/oauth2/source.go         |   4 +-
 .../auth/source/oauth2/source_authenticate.go |  43 +++
 .../auth/source/oauth2/source_register.go     |   5 +-
 services/auth/source/oauth2/token.go          |  94 ++++++
 services/auth/source/oauth2/urlmapping.go     |  24 ++
 services/auth/source/pam/source.go            |   1 +
 services/auth/source/smtp/source.go           |   1 +
 22 files changed, 523 insertions(+), 535 deletions(-)
 create mode 100644 models/store.go
 delete mode 100644 modules/auth/oauth2/oauth2.go
 create mode 100644 services/auth/source/oauth2/init.go
 rename {modules/auth => services/auth/source}/oauth2/jwtsigningkey.go (100%)
 create mode 100644 services/auth/source/oauth2/source_authenticate.go
 create mode 100644 services/auth/source/oauth2/token.go
 create mode 100644 services/auth/source/oauth2/urlmapping.go

diff --git a/cmd/admin.go b/cmd/admin.go
index da04e181275d7..5a0da5cb8d97e 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -14,8 +14,7 @@ import (
 	"text/tabwriter"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
-	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
@@ -599,7 +598,7 @@ func runRegenerateKeys(_ *cli.Context) error {
 	return models.RewriteAllPublicKeys()
 }
 
-func parseOAuth2Config(c *cli.Context) *oauth2Service.Source {
+func parseOAuth2Config(c *cli.Context) *oauth2.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if c.IsSet("use-custom-urls") {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -611,7 +610,7 @@ func parseOAuth2Config(c *cli.Context) *oauth2Service.Source {
 	} else {
 		customURLMapping = nil
 	}
-	return &oauth2Service.Source{
+	return &oauth2.Source{
 		Provider:                      c.String("provider"),
 		ClientID:                      c.String("key"),
 		ClientSecret:                  c.String("secret"),
@@ -648,7 +647,7 @@ func runUpdateOauth(c *cli.Context) error {
 		return err
 	}
 
-	oAuth2Config := source.Cfg.(*oauth2Service.Source)
+	oAuth2Config := source.Cfg.(*oauth2.Source)
 
 	if c.IsSet("name") {
 		source.Name = c.String("name")
diff --git a/models/oauth2.go b/models/oauth2.go
index e2e6f849bedcd..ad5761e525aa5 100644
--- a/models/oauth2.go
+++ b/models/oauth2.go
@@ -4,83 +4,6 @@
 
 package models
 
-import (
-	"code.gitea.io/gitea/modules/auth/oauth2"
-	"code.gitea.io/gitea/modules/log"
-)
-
-// OAuth2Provider describes the display values of a single OAuth2 provider
-type OAuth2Provider struct {
-	Name             string
-	DisplayName      string
-	Image            string
-	CustomURLMapping *oauth2.CustomURLMapping
-}
-
-// OAuth2Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
-// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
-// value is used to store display data
-var OAuth2Providers = map[string]OAuth2Provider{
-	"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
-	"dropbox":   {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
-	"facebook":  {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
-	"github": {
-		Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
-		CustomURLMapping: &oauth2.CustomURLMapping{
-			TokenURL:   oauth2.GetDefaultTokenURL("github"),
-			AuthURL:    oauth2.GetDefaultAuthURL("github"),
-			ProfileURL: oauth2.GetDefaultProfileURL("github"),
-			EmailURL:   oauth2.GetDefaultEmailURL("github"),
-		},
-	},
-	"gitlab": {
-		Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
-		CustomURLMapping: &oauth2.CustomURLMapping{
-			TokenURL:   oauth2.GetDefaultTokenURL("gitlab"),
-			AuthURL:    oauth2.GetDefaultAuthURL("gitlab"),
-			ProfileURL: oauth2.GetDefaultProfileURL("gitlab"),
-		},
-	},
-	"gplus":         {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
-	"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
-	"twitter":       {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
-	"discord":       {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
-	"gitea": {
-		Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
-		CustomURLMapping: &oauth2.CustomURLMapping{
-			TokenURL:   oauth2.GetDefaultTokenURL("gitea"),
-			AuthURL:    oauth2.GetDefaultAuthURL("gitea"),
-			ProfileURL: oauth2.GetDefaultProfileURL("gitea"),
-		},
-	},
-	"nextcloud": {
-		Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
-		CustomURLMapping: &oauth2.CustomURLMapping{
-			TokenURL:   oauth2.GetDefaultTokenURL("nextcloud"),
-			AuthURL:    oauth2.GetDefaultAuthURL("nextcloud"),
-			ProfileURL: oauth2.GetDefaultProfileURL("nextcloud"),
-		},
-	},
-	"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
-	"mastodon": {
-		Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
-		CustomURLMapping: &oauth2.CustomURLMapping{
-			AuthURL: oauth2.GetDefaultAuthURL("mastodon"),
-		},
-	},
-}
-
-// OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
-// key is used to map the OAuth2Provider
-// value is the mapping as defined for the OAuth2Provider
-var OAuth2DefaultCustomURLMappings = map[string]*oauth2.CustomURLMapping{
-	"github":    OAuth2Providers["github"].CustomURLMapping,
-	"gitlab":    OAuth2Providers["gitlab"].CustomURLMapping,
-	"gitea":     OAuth2Providers["gitea"].CustomURLMapping,
-	"nextcloud": OAuth2Providers["nextcloud"].CustomURLMapping,
-	"mastodon":  OAuth2Providers["mastodon"].CustomURLMapping,
-}
-
 // GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
 func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 1)
@@ -100,41 +23,3 @@ func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
 
 	return loginSource, nil
 }
-
-// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
-func InitOAuth2() error {
-	if err := oauth2.InitSigningKey(); err != nil {
-		return err
-	}
-	if err := oauth2.Init(x); err != nil {
-		return err
-	}
-	return initOAuth2LoginSources()
-}
-
-// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
-func ResetOAuth2() error {
-	oauth2.ClearProviders()
-	return initOAuth2LoginSources()
-}
-
-// initOAuth2LoginSources is used to load and register all active OAuth2 providers
-func initOAuth2LoginSources() error {
-	loginSources, _ := GetActiveOAuth2ProviderLoginSources()
-	for _, source := range loginSources {
-		registerableSource, ok := source.Cfg.(RegisterableSource)
-		if !ok {
-			continue
-		}
-		err := registerableSource.RegisterSource(source)
-		if err != nil {
-			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
-			source.IsActived = false
-			if err = UpdateSource(source); err != nil {
-				log.Critical("Unable to update source %s to disable it. Error: %v", err)
-				return err
-			}
-		}
-	}
-	return nil
-}
diff --git a/models/oauth2_application.go b/models/oauth2_application.go
index 3509dba54e2f8..b2e12cc3732fe 100644
--- a/models/oauth2_application.go
+++ b/models/oauth2_application.go
@@ -10,14 +10,11 @@ import (
 	"fmt"
 	"net/url"
 	"strings"
-	"time"
 
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/dgrijalva/jwt-go"
 	uuid "github.com/google/uuid"
 	"golang.org/x/crypto/bcrypt"
 	"xorm.io/xorm"
@@ -516,77 +513,3 @@ func revokeOAuth2Grant(e Engine, grantID, userID int64) error {
 	_, err := e.Delete(&OAuth2Grant{ID: grantID, UserID: userID})
 	return err
 }
-
-//////////////////////////////////////////////////////////////
-
-// OAuth2TokenType represents the type of token for an oauth application
-type OAuth2TokenType int
-
-const (
-	// TypeAccessToken is a token with short lifetime to access the api
-	TypeAccessToken OAuth2TokenType = 0
-	// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
-	TypeRefreshToken = iota
-)
-
-// OAuth2Token represents a JWT token used to authenticate a client
-type OAuth2Token struct {
-	GrantID int64           `json:"gnt"`
-	Type    OAuth2TokenType `json:"tt"`
-	Counter int64           `json:"cnt,omitempty"`
-	jwt.StandardClaims
-}
-
-// ParseOAuth2Token parses a singed jwt string
-func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) {
-	parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) {
-		if token.Method == nil || token.Method.Alg() != oauth2.DefaultSigningKey.SigningMethod().Alg() {
-			return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
-		}
-		return oauth2.DefaultSigningKey.VerifyKey(), nil
-	})
-	if err != nil {
-		return nil, err
-	}
-	var token *OAuth2Token
-	var ok bool
-	if token, ok = parsedToken.Claims.(*OAuth2Token); !ok || !parsedToken.Valid {
-		return nil, fmt.Errorf("invalid token")
-	}
-	return token, nil
-}
-
-// SignToken signs the token with the JWT secret
-func (token *OAuth2Token) SignToken() (string, error) {
-	token.IssuedAt = time.Now().Unix()
-	jwtToken := jwt.NewWithClaims(oauth2.DefaultSigningKey.SigningMethod(), token)
-	oauth2.DefaultSigningKey.PreProcessToken(jwtToken)
-	return jwtToken.SignedString(oauth2.DefaultSigningKey.SignKey())
-}
-
-// OIDCToken represents an OpenID Connect id_token
-type OIDCToken struct {
-	jwt.StandardClaims
-	Nonce string `json:"nonce,omitempty"`
-
-	// Scope profile
-	Name              string             `json:"name,omitempty"`
-	PreferredUsername string             `json:"preferred_username,omitempty"`
-	Profile           string             `json:"profile,omitempty"`
-	Picture           string             `json:"picture,omitempty"`
-	Website           string             `json:"website,omitempty"`
-	Locale            string             `json:"locale,omitempty"`
-	UpdatedAt         timeutil.TimeStamp `json:"updated_at,omitempty"`
-
-	// Scope email
-	Email         string `json:"email,omitempty"`
-	EmailVerified bool   `json:"email_verified,omitempty"`
-}
-
-// SignToken signs an id_token with the (symmetric) client secret key
-func (token *OIDCToken) SignToken(signingKey oauth2.JWTSigningKey) (string, error) {
-	token.IssuedAt = time.Now().Unix()
-	jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
-	signingKey.PreProcessToken(jwtToken)
-	return jwtToken.SignedString(signingKey.SignKey())
-}
diff --git a/models/store.go b/models/store.go
new file mode 100644
index 0000000000000..e8eba28fb6151
--- /dev/null
+++ b/models/store.go
@@ -0,0 +1,16 @@
+// Copyright 2021 The Gitea 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 models
+
+import "github.com/lafriks/xormstore"
+
+// CreateStore creates a xormstore for the provided table and key
+func CreateStore(table, key string) (*xormstore.Store, error) {
+	store, err := xormstore.NewOptions(x, xormstore.Options{
+		TableName: table,
+	}, []byte(key))
+
+	return store, err
+}
diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go
deleted file mode 100644
index 5d152e0a5588a..0000000000000
--- a/modules/auth/oauth2/oauth2.go
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright 2017 The Gitea 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 oauth2
-
-import (
-	"net/http"
-	"net/url"
-
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-
-	uuid "github.com/google/uuid"
-	"github.com/lafriks/xormstore"
-	"github.com/markbates/goth"
-	"github.com/markbates/goth/gothic"
-	"github.com/markbates/goth/providers/bitbucket"
-	"github.com/markbates/goth/providers/discord"
-	"github.com/markbates/goth/providers/dropbox"
-	"github.com/markbates/goth/providers/facebook"
-	"github.com/markbates/goth/providers/gitea"
-	"github.com/markbates/goth/providers/github"
-	"github.com/markbates/goth/providers/gitlab"
-	"github.com/markbates/goth/providers/google"
-	"github.com/markbates/goth/providers/mastodon"
-	"github.com/markbates/goth/providers/nextcloud"
-	"github.com/markbates/goth/providers/openidConnect"
-	"github.com/markbates/goth/providers/twitter"
-	"github.com/markbates/goth/providers/yandex"
-	"xorm.io/xorm"
-)
-
-var (
-	sessionUsersStoreKey = "gitea-oauth2-sessions"
-	providerHeaderKey    = "gitea-oauth2-provider"
-)
-
-// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
-type CustomURLMapping struct {
-	AuthURL    string
-	TokenURL   string
-	ProfileURL string
-	EmailURL   string
-}
-
-// Init initialize the setup of the OAuth2 library
-func Init(x *xorm.Engine) error {
-	store, err := xormstore.NewOptions(x, xormstore.Options{
-		TableName: "oauth2_session",
-	}, []byte(sessionUsersStoreKey))
-
-	if err != nil {
-		return err
-	}
-	// according to the Goth lib:
-	// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
-	// securecookie: the value is too long
-	// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
-
-	// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
-	store.MaxLength(setting.OAuth2.MaxTokenLength)
-	gothic.Store = store
-
-	gothic.SetState = func(req *http.Request) string {
-		return uuid.New().String()
-	}
-
-	gothic.GetProviderName = func(req *http.Request) (string, error) {
-		return req.Header.Get(providerHeaderKey), nil
-	}
-
-	return nil
-}
-
-// Auth OAuth2 auth service
-func Auth(provider string, request *http.Request, response http.ResponseWriter) error {
-	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(providerHeaderKey, provider)
-
-	// don't use the default gothic begin handler to prevent issues when some error occurs
-	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
-	//gothic.BeginAuthHandler(response, request)
-
-	url, err := gothic.GetAuthURL(response, request)
-	if err == nil {
-		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
-	}
-	return err
-}
-
-// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
-// this will trigger a new authentication request, but because we save it in the session we can use that
-func ProviderCallback(provider string, request *http.Request, response http.ResponseWriter) (goth.User, error) {
-	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(providerHeaderKey, provider)
-
-	user, err := gothic.CompleteUserAuth(response, request)
-	if err != nil {
-		return user, err
-	}
-
-	return user, nil
-}
-
-// RegisterProvider register a OAuth2 provider in goth lib
-func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error {
-	provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping)
-
-	if err == nil && provider != nil {
-		goth.UseProviders(provider)
-	}
-
-	return err
-}
-
-// RemoveProvider removes the given OAuth2 provider from the goth lib
-func RemoveProvider(providerName string) {
-	delete(goth.GetProviders(), providerName)
-}
-
-// ClearProviders clears all OAuth2 providers from the goth lib
-func ClearProviders() {
-	goth.ClearProviders()
-}
-
-// used to create different types of goth providers
-func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) (goth.Provider, error) {
-	callbackURL := setting.AppURL + "user/oauth2/" + url.PathEscape(providerName) + "/callback"
-
-	var provider goth.Provider
-	var err error
-
-	switch providerType {
-	case "bitbucket":
-		provider = bitbucket.New(clientID, clientSecret, callbackURL, "account")
-	case "dropbox":
-		provider = dropbox.New(clientID, clientSecret, callbackURL)
-	case "facebook":
-		provider = facebook.New(clientID, clientSecret, callbackURL, "email")
-	case "github":
-		authURL := github.AuthURL
-		tokenURL := github.TokenURL
-		profileURL := github.ProfileURL
-		emailURL := github.EmailURL
-		if customURLMapping != nil {
-			if len(customURLMapping.AuthURL) > 0 {
-				authURL = customURLMapping.AuthURL
-			}
-			if len(customURLMapping.TokenURL) > 0 {
-				tokenURL = customURLMapping.TokenURL
-			}
-			if len(customURLMapping.ProfileURL) > 0 {
-				profileURL = customURLMapping.ProfileURL
-			}
-			if len(customURLMapping.EmailURL) > 0 {
-				emailURL = customURLMapping.EmailURL
-			}
-		}
-		scopes := []string{}
-		if setting.OAuth2Client.EnableAutoRegistration {
-			scopes = append(scopes, "user:email")
-		}
-		provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL, scopes...)
-	case "gitlab":
-		authURL := gitlab.AuthURL
-		tokenURL := gitlab.TokenURL
-		profileURL := gitlab.ProfileURL
-		if customURLMapping != nil {
-			if len(customURLMapping.AuthURL) > 0 {
-				authURL = customURLMapping.AuthURL
-			}
-			if len(customURLMapping.TokenURL) > 0 {
-				tokenURL = customURLMapping.TokenURL
-			}
-			if len(customURLMapping.ProfileURL) > 0 {
-				profileURL = customURLMapping.ProfileURL
-			}
-		}
-		provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user")
-	case "gplus": // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work
-		scopes := []string{"email"}
-		if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration {
-			scopes = append(scopes, "profile")
-		}
-		provider = google.New(clientID, clientSecret, callbackURL, scopes...)
-	case "openidConnect":
-		if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...); err != nil {
-			log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err)
-		}
-	case "twitter":
-		provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL)
-	case "discord":
-		provider = discord.New(clientID, clientSecret, callbackURL, discord.ScopeIdentify, discord.ScopeEmail)
-	case "gitea":
-		authURL := gitea.AuthURL
-		tokenURL := gitea.TokenURL
-		profileURL := gitea.ProfileURL
-		if customURLMapping != nil {
-			if len(customURLMapping.AuthURL) > 0 {
-				authURL = customURLMapping.AuthURL
-			}
-			if len(customURLMapping.TokenURL) > 0 {
-				tokenURL = customURLMapping.TokenURL
-			}
-			if len(customURLMapping.ProfileURL) > 0 {
-				profileURL = customURLMapping.ProfileURL
-			}
-		}
-		provider = gitea.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
-	case "nextcloud":
-		authURL := nextcloud.AuthURL
-		tokenURL := nextcloud.TokenURL
-		profileURL := nextcloud.ProfileURL
-		if customURLMapping != nil {
-			if len(customURLMapping.AuthURL) > 0 {
-				authURL = customURLMapping.AuthURL
-			}
-			if len(customURLMapping.TokenURL) > 0 {
-				tokenURL = customURLMapping.TokenURL
-			}
-			if len(customURLMapping.ProfileURL) > 0 {
-				profileURL = customURLMapping.ProfileURL
-			}
-		}
-		provider = nextcloud.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
-	case "yandex":
-		// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
-		provider = yandex.New(clientID, clientSecret, callbackURL, "login:email", "login:info", "login:avatar")
-	case "mastodon":
-		instanceURL := mastodon.InstanceURL
-		if customURLMapping != nil && len(customURLMapping.AuthURL) > 0 {
-			instanceURL = customURLMapping.AuthURL
-		}
-		provider = mastodon.NewCustomisedURL(clientID, clientSecret, callbackURL, instanceURL)
-	}
-
-	// always set the name if provider is created so we can support multiple setups of 1 provider
-	if err == nil && provider != nil {
-		provider.SetName(providerName)
-	}
-
-	return provider, err
-}
-
-// GetDefaultTokenURL return the default token url for the given provider
-func GetDefaultTokenURL(provider string) string {
-	switch provider {
-	case "github":
-		return github.TokenURL
-	case "gitlab":
-		return gitlab.TokenURL
-	case "gitea":
-		return gitea.TokenURL
-	case "nextcloud":
-		return nextcloud.TokenURL
-	}
-	return ""
-}
-
-// GetDefaultAuthURL return the default authorize url for the given provider
-func GetDefaultAuthURL(provider string) string {
-	switch provider {
-	case "github":
-		return github.AuthURL
-	case "gitlab":
-		return gitlab.AuthURL
-	case "gitea":
-		return gitea.AuthURL
-	case "nextcloud":
-		return nextcloud.AuthURL
-	case "mastodon":
-		return mastodon.InstanceURL
-	}
-	return ""
-}
-
-// GetDefaultProfileURL return the default profile url for the given provider
-func GetDefaultProfileURL(provider string) string {
-	switch provider {
-	case "github":
-		return github.ProfileURL
-	case "gitlab":
-		return gitlab.ProfileURL
-	case "gitea":
-		return gitea.ProfileURL
-	case "nextcloud":
-		return nextcloud.ProfileURL
-	}
-	return ""
-}
-
-// GetDefaultEmailURL return the default email url for the given provider
-func GetDefaultEmailURL(provider string) string {
-	if provider == "github" {
-		return github.EmailURL
-	}
-	return ""
-}
diff --git a/routers/init.go b/routers/init.go
index 4c28a953955ba..a238271e3a4d1 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -34,6 +34,7 @@ import (
 	"code.gitea.io/gitea/routers/private"
 	web_routers "code.gitea.io/gitea/routers/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/mailer"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -102,7 +103,7 @@ func GlobalInit(ctx context.Context) {
 		log.Fatal("ORM engine initialization failed: %v", err)
 	}
 
-	if err := models.InitOAuth2(); err != nil {
+	if err := oauth2.Init(); err != nil {
 		log.Fatal("Failed to initialize OAuth2 support: %v", err)
 	}
 
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index a8d3f7cd8801d..19c24ce251c3b 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -11,7 +11,6 @@ import (
 	"regexp"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/auth/pam"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
@@ -20,7 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth/source/ldap"
-	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 	pamService "code.gitea.io/gitea/services/auth/source/pam"
 	"code.gitea.io/gitea/services/auth/source/smtp"
 	"code.gitea.io/gitea/services/auth/source/sspi"
@@ -99,8 +98,8 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -109,7 +108,7 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.Data["SSPIDefaultLanguage"] = ""
 
 	// only the first as default
-	for key := range models.OAuth2Providers {
+	for key := range oauth2.Providers {
 		ctx.Data["oauth2_provider"] = key
 		break
 	}
@@ -163,7 +162,7 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
 	}
 }
 
-func parseOAuth2Config(form forms.AuthenticationForm) *oauth2Service.Source {
+func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if form.Oauth2UseCustomURL {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -175,7 +174,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2Service.Source {
 	} else {
 		customURLMapping = nil
 	}
-	return &oauth2Service.Source{
+	return &oauth2.Source{
 		Provider:                      form.Oauth2Provider,
 		ClientID:                      form.Oauth2Key,
 		ClientSecret:                  form.Oauth2Secret,
@@ -221,8 +220,8 @@ func NewAuthSourcePost(ctx *context.Context) {
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -300,8 +299,8 @@ func EditAuthSource(ctx *context.Context) {
 
 	ctx.Data["SecurityProtocols"] = securityProtocols
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 	if err != nil {
@@ -312,7 +311,7 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["HasTLS"] = source.HasTLS()
 
 	if source.IsOAuth2() {
-		ctx.Data["CurrentOAuth2Provider"] = models.OAuth2Providers[source.Cfg.(*oauth2Service.Source).Provider]
+		ctx.Data["CurrentOAuth2Provider"] = oauth2.Providers[source.Cfg.(*oauth2.Source).Provider]
 	}
 	ctx.HTML(http.StatusOK, tplAuthEdit)
 }
@@ -325,8 +324,8 @@ func EditAuthSourcePost(ctx *context.Context) {
 	ctx.Data["PageIsAdminAuthentications"] = true
 
 	ctx.Data["SMTPAuths"] = smtp.Authenticators
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 	if err != nil {
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 5034347bb4178..723424046b1e6 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -14,7 +14,6 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/eventsource"
@@ -579,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
 		return
 	}
 
-	if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+	if err = loginSource.Cfg.(*oauth2Service.Source).Authenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
 		if strings.Contains(err.Error(), "no provider for ") {
-			if err = models.ResetOAuth2(); err != nil {
+			if err = oauth2Service.ResetOAuth2(); err != nil {
 				ctx.ServerError("SignIn", err)
 				return
 			}
-			if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+			if err = loginSource.Cfg.(*oauth2Service.Source).Authenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
 				ctx.ServerError("SignIn", err)
 			}
 			return
@@ -774,8 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
 // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 // login the user
 func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
-	gothUser, err := oauth2.ProviderCallback(loginSource.Name, request, response)
-
+	gothUser, err := loginSource.Cfg.(*oauth2Service.Source).ProviderCallback(loginSource, request, response)
 	if err != nil {
 		if err.Error() == "securecookie: the value is too long" {
 			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
diff --git a/routers/web/user/oauth.go b/routers/web/user/oauth.go
index 72295b4447c28..7e108f6e7830d 100644
--- a/routers/web/user/oauth.go
+++ b/routers/web/user/oauth.go
@@ -13,7 +13,6 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
@@ -21,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/forms"
 
 	"gitea.com/go-chi/binding"
@@ -144,9 +144,9 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 	}
 	// generate access token to access the API
 	expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
-	accessToken := &models.OAuth2Token{
+	accessToken := &oauth2.Token{
 		GrantID: grant.ID,
-		Type:    models.TypeAccessToken,
+		Type:    oauth2.TypeAccessToken,
 		StandardClaims: jwt.StandardClaims{
 			ExpiresAt: expirationDate.AsTime().Unix(),
 		},
@@ -161,10 +161,10 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 
 	// generate refresh token to request an access token after it expired later
 	refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
-	refreshToken := &models.OAuth2Token{
+	refreshToken := &oauth2.Token{
 		GrantID: grant.ID,
 		Counter: grant.Counter,
-		Type:    models.TypeRefreshToken,
+		Type:    oauth2.TypeRefreshToken,
 		StandardClaims: jwt.StandardClaims{
 			ExpiresAt: refreshExpirationDate,
 		},
@@ -202,7 +202,7 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSign
 			}
 		}
 
-		idToken := &models.OIDCToken{
+		idToken := &oauth2.OIDCToken{
 			StandardClaims: jwt.StandardClaims{
 				ExpiresAt: expirationDate.AsTime().Unix(),
 				Issuer:    setting.AppURL,
@@ -568,7 +568,7 @@ func AccessTokenOAuth(ctx *context.Context) {
 }
 
 func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) {
-	token, err := models.ParseOAuth2Token(form.RefreshToken)
+	token, err := oauth2.ParseToken(form.RefreshToken)
 	if err != nil {
 		handleAccessTokenError(ctx, AccessTokenError{
 			ErrorCode:        AccessTokenErrorCodeUnauthorizedClient,
diff --git a/routers/web/user/setting/security.go b/routers/web/user/setting/security.go
index d1c61a9a4c4f2..dd5d2a20ccccc 100644
--- a/routers/web/user/setting/security.go
+++ b/routers/web/user/setting/security.go
@@ -94,7 +94,7 @@ func loadSecurityData(ctx *context.Context) {
 			var providerDisplayName string
 			if loginSource.IsOAuth2() {
 				providerTechnicalName := loginSource.Cfg.(*oauth2.Source).Provider
-				providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
+				providerDisplayName = oauth2.Providers[providerTechnicalName].DisplayName
 			} else {
 				providerDisplayName = loginSource.Name
 			}
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index e9f4c69e8886d..b6b922de7a4d6 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 )
 
 // Ensure the struct implements the interface.
@@ -27,7 +28,7 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 	if !strings.Contains(accessToken, ".") {
 		return 0
 	}
-	token, err := models.ParseOAuth2Token(accessToken)
+	token, err := oauth2.ParseToken(accessToken)
 	if err != nil {
 		log.Trace("ParseOAuth2Token: %v", err)
 		return 0
@@ -36,7 +37,7 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 	if grant, err = models.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil {
 		return 0
 	}
-	if token.Type != models.TypeAccessToken {
+	if token.Type != oauth2.TypeAccessToken {
 		return 0
 	}
 	if token.ExpiresAt < time.Now().Unix() || token.IssuedAt > time.Now().Unix() {
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index fc6a62708fe56..4fd218cbe949c 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/secret"
 	"code.gitea.io/gitea/modules/setting"
+
 	jsoniter "github.com/json-iterator/go"
 )
 
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
new file mode 100644
index 0000000000000..15d16a623c80a
--- /dev/null
+++ b/services/auth/source/oauth2/init.go
@@ -0,0 +1,83 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/google/uuid"
+	"github.com/markbates/goth/gothic"
+)
+
+// SessionTableName is the table name that OAuth2 will use to store things
+const SessionTableName = "oauth2_session"
+
+// UsersStoreKey is the key for the store
+const UsersStoreKey = "gitea-oauth2-sessions"
+
+// ProviderHeaderKey is the HTTP header key
+const ProviderHeaderKey = "gitea-oauth2-provider"
+
+// Init initializes the oauth source
+func Init() error {
+	if err := InitSigningKey(); err != nil {
+		return err
+	}
+
+	store, err := models.CreateStore(SessionTableName, UsersStoreKey)
+	if err != nil {
+		return err
+	}
+
+	// according to the Goth lib:
+	// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
+	// securecookie: the value is too long
+	// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
+
+	// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
+	store.MaxLength(setting.OAuth2.MaxTokenLength)
+	gothic.Store = store
+
+	gothic.SetState = func(req *http.Request) string {
+		return uuid.New().String()
+	}
+
+	gothic.GetProviderName = func(req *http.Request) (string, error) {
+		return req.Header.Get(ProviderHeaderKey), nil
+	}
+
+	return initOAuth2LoginSources()
+}
+
+// ResetOAuth2 clears existing OAuth2 providers and loads them from DB
+func ResetOAuth2() error {
+	ClearProviders()
+	return initOAuth2LoginSources()
+}
+
+// initOAuth2LoginSources is used to load and register all active OAuth2 providers
+func initOAuth2LoginSources() error {
+	loginSources, _ := models.GetActiveOAuth2ProviderLoginSources()
+	for _, source := range loginSources {
+		oauth2Source, ok := source.Cfg.(*Source)
+		if !ok {
+			continue
+		}
+		err := oauth2Source.RegisterSource(source)
+		if err != nil {
+			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
+			source.IsActived = false
+			if err = models.UpdateSource(source); err != nil {
+				log.Critical("Unable to update source %s to disable it. Error: %v", err)
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/modules/auth/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
similarity index 100%
rename from modules/auth/oauth2/jwtsigningkey.go
rename to services/auth/source/oauth2/jwtsigningkey.go
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index 69340cd09567a..bf97f8002aa34 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -5,15 +5,94 @@
 package oauth2
 
 import (
+	"net/url"
 	"sort"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/markbates/goth"
+	"github.com/markbates/goth/providers/bitbucket"
+	"github.com/markbates/goth/providers/discord"
+	"github.com/markbates/goth/providers/dropbox"
+	"github.com/markbates/goth/providers/facebook"
+	"github.com/markbates/goth/providers/gitea"
+	"github.com/markbates/goth/providers/github"
+	"github.com/markbates/goth/providers/gitlab"
+	"github.com/markbates/goth/providers/google"
+	"github.com/markbates/goth/providers/mastodon"
+	"github.com/markbates/goth/providers/nextcloud"
+	"github.com/markbates/goth/providers/openidConnect"
+	"github.com/markbates/goth/providers/twitter"
+	"github.com/markbates/goth/providers/yandex"
 )
 
+// Provider describes the display values of a single OAuth2 provider
+type Provider struct {
+	Name             string
+	DisplayName      string
+	Image            string
+	CustomURLMapping *CustomURLMapping
+}
+
+// Providers contains the map of registered OAuth2 providers in Gitea (based on goth)
+// key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider)
+// value is used to store display data
+var Providers = map[string]Provider{
+	"bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"},
+	"dropbox":   {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"},
+	"facebook":  {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"},
+	"github": {
+		Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png",
+		CustomURLMapping: &CustomURLMapping{
+			TokenURL:   github.TokenURL,
+			AuthURL:    github.AuthURL,
+			ProfileURL: github.ProfileURL,
+			EmailURL:   github.EmailURL,
+		},
+	},
+	"gitlab": {
+		Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png",
+		CustomURLMapping: &CustomURLMapping{
+			TokenURL:   gitlab.TokenURL,
+			AuthURL:    gitlab.AuthURL,
+			ProfileURL: gitlab.ProfileURL,
+		},
+	},
+	"gplus":         {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"},
+	"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"},
+	"twitter":       {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"},
+	"discord":       {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"},
+	"gitea": {
+		Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png",
+		CustomURLMapping: &CustomURLMapping{
+			TokenURL:   gitea.TokenURL,
+			AuthURL:    gitea.AuthURL,
+			ProfileURL: gitea.ProfileURL,
+		},
+	},
+	"nextcloud": {
+		Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png",
+		CustomURLMapping: &CustomURLMapping{
+			TokenURL:   nextcloud.TokenURL,
+			AuthURL:    nextcloud.AuthURL,
+			ProfileURL: nextcloud.ProfileURL,
+		},
+	},
+	"yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"},
+	"mastodon": {
+		Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png",
+		CustomURLMapping: &CustomURLMapping{
+			AuthURL: mastodon.InstanceURL,
+		},
+	},
+}
+
 // GetActiveOAuth2Providers returns the map of configured active OAuth2 providers
 // key is used as technical name (like in the callbackURL)
 // values to display
-func GetActiveOAuth2Providers() ([]string, map[string]models.OAuth2Provider, error) {
+func GetActiveOAuth2Providers() ([]string, map[string]Provider, error) {
 	// Maybe also separate used and unused providers so we can force the registration of only 1 active provider for each type
 
 	loginSources, err := models.GetActiveOAuth2ProviderLoginSources()
@@ -22,9 +101,9 @@ func GetActiveOAuth2Providers() ([]string, map[string]models.OAuth2Provider, err
 	}
 
 	var orderedKeys []string
-	providers := make(map[string]models.OAuth2Provider)
+	providers := make(map[string]Provider)
 	for _, source := range loginSources {
-		prov := models.OAuth2Providers[source.Cfg.(*Source).Provider]
+		prov := Providers[source.Cfg.(*Source).Provider]
 		if source.Cfg.(*Source).IconURL != "" {
 			prov.Image = source.Cfg.(*Source).IconURL
 		}
@@ -36,3 +115,143 @@ func GetActiveOAuth2Providers() ([]string, map[string]models.OAuth2Provider, err
 
 	return orderedKeys, providers, nil
 }
+
+// RegisterProvider register a OAuth2 provider in goth lib
+func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error {
+	provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping)
+
+	if err == nil && provider != nil {
+		goth.UseProviders(provider)
+	}
+
+	return err
+}
+
+// RemoveProvider removes the given OAuth2 provider from the goth lib
+func RemoveProvider(providerName string) {
+	delete(goth.GetProviders(), providerName)
+}
+
+// ClearProviders clears all OAuth2 providers from the goth lib
+func ClearProviders() {
+	goth.ClearProviders()
+}
+
+// used to create different types of goth providers
+func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) (goth.Provider, error) {
+	callbackURL := setting.AppURL + "user/oauth2/" + url.PathEscape(providerName) + "/callback"
+
+	var provider goth.Provider
+	var err error
+
+	switch providerType {
+	case "bitbucket":
+		provider = bitbucket.New(clientID, clientSecret, callbackURL, "account")
+	case "dropbox":
+		provider = dropbox.New(clientID, clientSecret, callbackURL)
+	case "facebook":
+		provider = facebook.New(clientID, clientSecret, callbackURL, "email")
+	case "github":
+		authURL := github.AuthURL
+		tokenURL := github.TokenURL
+		profileURL := github.ProfileURL
+		emailURL := github.EmailURL
+		if customURLMapping != nil {
+			if len(customURLMapping.AuthURL) > 0 {
+				authURL = customURLMapping.AuthURL
+			}
+			if len(customURLMapping.TokenURL) > 0 {
+				tokenURL = customURLMapping.TokenURL
+			}
+			if len(customURLMapping.ProfileURL) > 0 {
+				profileURL = customURLMapping.ProfileURL
+			}
+			if len(customURLMapping.EmailURL) > 0 {
+				emailURL = customURLMapping.EmailURL
+			}
+		}
+		scopes := []string{}
+		if setting.OAuth2Client.EnableAutoRegistration {
+			scopes = append(scopes, "user:email")
+		}
+		provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL, scopes...)
+	case "gitlab":
+		authURL := gitlab.AuthURL
+		tokenURL := gitlab.TokenURL
+		profileURL := gitlab.ProfileURL
+		if customURLMapping != nil {
+			if len(customURLMapping.AuthURL) > 0 {
+				authURL = customURLMapping.AuthURL
+			}
+			if len(customURLMapping.TokenURL) > 0 {
+				tokenURL = customURLMapping.TokenURL
+			}
+			if len(customURLMapping.ProfileURL) > 0 {
+				profileURL = customURLMapping.ProfileURL
+			}
+		}
+		provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user")
+	case "gplus": // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work
+		scopes := []string{"email"}
+		if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration {
+			scopes = append(scopes, "profile")
+		}
+		provider = google.New(clientID, clientSecret, callbackURL, scopes...)
+	case "openidConnect":
+		if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...); err != nil {
+			log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err)
+		}
+	case "twitter":
+		provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL)
+	case "discord":
+		provider = discord.New(clientID, clientSecret, callbackURL, discord.ScopeIdentify, discord.ScopeEmail)
+	case "gitea":
+		authURL := gitea.AuthURL
+		tokenURL := gitea.TokenURL
+		profileURL := gitea.ProfileURL
+		if customURLMapping != nil {
+			if len(customURLMapping.AuthURL) > 0 {
+				authURL = customURLMapping.AuthURL
+			}
+			if len(customURLMapping.TokenURL) > 0 {
+				tokenURL = customURLMapping.TokenURL
+			}
+			if len(customURLMapping.ProfileURL) > 0 {
+				profileURL = customURLMapping.ProfileURL
+			}
+		}
+		provider = gitea.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
+	case "nextcloud":
+		authURL := nextcloud.AuthURL
+		tokenURL := nextcloud.TokenURL
+		profileURL := nextcloud.ProfileURL
+		if customURLMapping != nil {
+			if len(customURLMapping.AuthURL) > 0 {
+				authURL = customURLMapping.AuthURL
+			}
+			if len(customURLMapping.TokenURL) > 0 {
+				tokenURL = customURLMapping.TokenURL
+			}
+			if len(customURLMapping.ProfileURL) > 0 {
+				profileURL = customURLMapping.ProfileURL
+			}
+		}
+		provider = nextcloud.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL)
+	case "yandex":
+		// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
+		provider = yandex.New(clientID, clientSecret, callbackURL, "login:email", "login:info", "login:avatar")
+	case "mastodon":
+		instanceURL := mastodon.InstanceURL
+		if customURLMapping != nil && len(customURLMapping.AuthURL) > 0 {
+			instanceURL = customURLMapping.AuthURL
+		}
+		provider = mastodon.NewCustomisedURL(clientID, clientSecret, callbackURL, instanceURL)
+	}
+
+	// always set the name if provider is created so we can support multiple setups of 1 provider
+	if err == nil && provider != nil {
+		provider.SetName(providerName)
+	}
+
+	return provider, err
+}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 81e899244a4c9..3a5c221c366d7 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -6,7 +6,7 @@ package oauth2
 
 import (
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
+
 	jsoniter "github.com/json-iterator/go"
 )
 
@@ -23,7 +23,7 @@ type Source struct {
 	ClientID                      string
 	ClientSecret                  string
 	OpenIDConnectAutoDiscoveryURL string
-	CustomURLMapping              *oauth2.CustomURLMapping
+	CustomURLMapping              *CustomURLMapping
 	IconURL                       string
 }
 
diff --git a/services/auth/source/oauth2/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
new file mode 100644
index 0000000000000..fa775d9d3d539
--- /dev/null
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -0,0 +1,43 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/models"
+	"github.com/markbates/goth"
+	"github.com/markbates/goth/gothic"
+)
+
+// Authenticate takes a provided loginSource and the request/response pair to authenticate against the provider
+func (source *Source) Authenticate(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) error {
+	// not sure if goth is thread safe (?) when using multiple providers
+	request.Header.Set(ProviderHeaderKey, loginSource.Name)
+
+	// don't use the default gothic begin handler to prevent issues when some error occurs
+	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
+	//gothic.BeginAuthHandler(response, request)
+
+	url, err := gothic.GetAuthURL(response, request)
+	if err == nil {
+		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
+	}
+	return err
+}
+
+// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
+// this will trigger a new authentication request, but because we save it in the session we can use that
+func (source *Source) ProviderCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (goth.User, error) {
+	// not sure if goth is thread safe (?) when using multiple providers
+	request.Header.Set(ProviderHeaderKey, loginSource.Name)
+
+	user, err := gothic.CompleteUserAuth(response, request)
+	if err != nil {
+		return user, err
+	}
+
+	return user, nil
+}
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
index 538cdfd6c23b1..6df1abc63cb96 100644
--- a/services/auth/source/oauth2/source_register.go
+++ b/services/auth/source/oauth2/source_register.go
@@ -6,18 +6,17 @@ package oauth2
 
 import (
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
 )
 
 // RegisterSource causes an OAuth2 configuration to be registered
 func (source *Source) RegisterSource(loginSource *models.LoginSource) error {
-	err := oauth2.RegisterProvider(loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
+	err := RegisterProvider(loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
 	return wrapOpenIDConnectInitializeError(err, loginSource.Name, source)
 }
 
 // UnregisterSource causes an OAuth2 configuration to be unregistered
 func (source *Source) UnregisterSource(loginSource *models.LoginSource) error {
-	oauth2.RemoveProvider(loginSource.Name)
+	RemoveProvider(loginSource.Name)
 	return nil
 }
 
diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go
new file mode 100644
index 0000000000000..e2ac8b9ca5dd1
--- /dev/null
+++ b/services/auth/source/oauth2/token.go
@@ -0,0 +1,94 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/modules/timeutil"
+	"github.com/dgrijalva/jwt-go"
+)
+
+// ___________     __
+// \__    ___/___ |  | __ ____   ____
+//   |    | /  _ \|  |/ // __ \ /    \
+//   |    |(  <_> )    <\  ___/|   |  \
+//   |____| \____/|__|_ \\___  >___|  /
+//                     \/    \/     \/
+
+// Token represents an Oauth grant
+
+// TokenType represents the type of token for an oauth application
+type TokenType int
+
+const (
+	// TypeAccessToken is a token with short lifetime to access the api
+	TypeAccessToken TokenType = 0
+	// TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
+	TypeRefreshToken = iota
+)
+
+// Token represents a JWT token used to authenticate a client
+type Token struct {
+	GrantID int64     `json:"gnt"`
+	Type    TokenType `json:"tt"`
+	Counter int64     `json:"cnt,omitempty"`
+	jwt.StandardClaims
+}
+
+// ParseToken parses a singed jwt string
+func ParseToken(jwtToken string) (*Token, error) {
+	parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) {
+		if token.Method == nil || token.Method.Alg() != DefaultSigningKey.SigningMethod().Alg() {
+			return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
+		}
+		return DefaultSigningKey.VerifyKey(), nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	var token *Token
+	var ok bool
+	if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
+		return nil, fmt.Errorf("invalid token")
+	}
+	return token, nil
+}
+
+// SignToken signs the token with the JWT secret
+func (token *Token) SignToken() (string, error) {
+	token.IssuedAt = time.Now().Unix()
+	jwtToken := jwt.NewWithClaims(DefaultSigningKey.SigningMethod(), token)
+	DefaultSigningKey.PreProcessToken(jwtToken)
+	return jwtToken.SignedString(DefaultSigningKey.SignKey())
+}
+
+// OIDCToken represents an OpenID Connect id_token
+type OIDCToken struct {
+	jwt.StandardClaims
+	Nonce string `json:"nonce,omitempty"`
+
+	// Scope profile
+	Name              string             `json:"name,omitempty"`
+	PreferredUsername string             `json:"preferred_username,omitempty"`
+	Profile           string             `json:"profile,omitempty"`
+	Picture           string             `json:"picture,omitempty"`
+	Website           string             `json:"website,omitempty"`
+	Locale            string             `json:"locale,omitempty"`
+	UpdatedAt         timeutil.TimeStamp `json:"updated_at,omitempty"`
+
+	// Scope email
+	Email         string `json:"email,omitempty"`
+	EmailVerified bool   `json:"email_verified,omitempty"`
+}
+
+// SignToken signs an id_token with the (symmetric) client secret key
+func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
+	token.IssuedAt = time.Now().Unix()
+	jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
+	signingKey.PreProcessToken(jwtToken)
+	return jwtToken.SignedString(signingKey.SignKey())
+}
diff --git a/services/auth/source/oauth2/urlmapping.go b/services/auth/source/oauth2/urlmapping.go
new file mode 100644
index 0000000000000..68829fba2167f
--- /dev/null
+++ b/services/auth/source/oauth2/urlmapping.go
@@ -0,0 +1,24 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+// CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs
+type CustomURLMapping struct {
+	AuthURL    string
+	TokenURL   string
+	ProfileURL string
+	EmailURL   string
+}
+
+// DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
+// key is used to map the OAuth2Provider
+// value is the mapping as defined for the OAuth2Provider
+var DefaultCustomURLMappings = map[string]*CustomURLMapping{
+	"github":    Providers["github"].CustomURLMapping,
+	"gitlab":    Providers["gitlab"].CustomURLMapping,
+	"gitea":     Providers["gitea"].CustomURLMapping,
+	"nextcloud": Providers["nextcloud"].CustomURLMapping,
+	"mastodon":  Providers["mastodon"].CustomURLMapping,
+}
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index f6b098e562af3..f47ea9fb18d07 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -6,6 +6,7 @@ package pam
 
 import (
 	"code.gitea.io/gitea/models"
+
 	jsoniter "github.com/json-iterator/go"
 )
 
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 77f2b73cd9d99..b3aabe6f2793c 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -6,6 +6,7 @@ package smtp
 
 import (
 	"code.gitea.io/gitea/models"
+
 	jsoniter "github.com/json-iterator/go"
 )
 

From 743692a1b5df73fc1a7737aeff3f72c14582e0b5 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 17:20:17 +0100
Subject: [PATCH 14/44] fixup! Extract out login-sources from models

---
 services/auth/source/db/{login.go => authenticate.go} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename services/auth/source/db/{login.go => authenticate.go} (100%)

diff --git a/services/auth/source/db/login.go b/services/auth/source/db/authenticate.go
similarity index 100%
rename from services/auth/source/db/login.go
rename to services/auth/source/db/authenticate.go

From 5ceec6653e23f1e98887205a3b143d039d642b58 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 17:20:52 +0100
Subject: [PATCH 15/44] fixup! Extract out login-sources from models

---
 services/auth/interface.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/services/auth/interface.go b/services/auth/interface.go
index 0488a8b716356..41a28d1a7119a 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -39,8 +39,8 @@ type Method interface {
 	Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
 }
 
-// Authenticator represents a source of authentication
-type Authenticator interface {
+// PasswordAuthenticator represents a source of authentication
+type PasswordAuthenticator interface {
 	Authenticate(user *models.User, login, password string, source *models.LoginSource) (*models.User, error)
 }
 

From 7c47d46a056d25cc87489a82b3c4ccbf37e2f164 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 17:21:36 +0100
Subject: [PATCH 16/44] fixup! Extract out login-sources from models

---
 services/auth/signin.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/services/auth/signin.go b/services/auth/signin.go
index 22529a4b9e5fa..521b7c01d4dad 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -62,7 +62,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 				return nil, models.ErrLoginSourceNotActived
 			}
 
-			authenticator, ok := source.Cfg.(Authenticator)
+			authenticator, ok := source.Cfg.(PasswordAuthenticator)
 			if !ok {
 				return nil, models.ErrUnsupportedLoginType
 
@@ -94,7 +94,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 			continue
 		}
 
-		authenticator, ok := source.Cfg.(Authenticator)
+		authenticator, ok := source.Cfg.(PasswordAuthenticator)
 		if !ok {
 			continue
 		}

From ee3871d9bc2de24927155a20d36d56543ece05e9 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 17:21:59 +0100
Subject: [PATCH 17/44] fixup! Remove modules/auth/oauth2 and begin clean up of
 oauth2

---
 routers/web/user/auth.go                         | 16 ++++++++--------
 .../auth/source/oauth2/source_authenticate.go    |  4 ++--
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 723424046b1e6..8210dc280a7c5 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -27,7 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/web/middleware"
 	"code.gitea.io/gitea/routers/utils"
 	"code.gitea.io/gitea/services/auth"
-	oauth2Service "code.gitea.io/gitea/services/auth/source/oauth2"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -136,7 +136,7 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
-	orderedOAuth2Names, oauth2Providers, err := oauth2Service.GetActiveOAuth2Providers()
+	orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -156,7 +156,7 @@ func SignIn(ctx *context.Context) {
 func SignInPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("sign_in")
 
-	orderedOAuth2Names, oauth2Providers, err := oauth2Service.GetActiveOAuth2Providers()
+	orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -578,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
 		return
 	}
 
-	if err = loginSource.Cfg.(*oauth2Service.Source).Authenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
+	if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
 		if strings.Contains(err.Error(), "no provider for ") {
-			if err = oauth2Service.ResetOAuth2(); err != nil {
+			if err = oauth2.ResetOAuth2(); err != nil {
 				ctx.ServerError("SignIn", err)
 				return
 			}
-			if err = loginSource.Cfg.(*oauth2Service.Source).Authenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
+			if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
 				ctx.ServerError("SignIn", err)
 			}
 			return
@@ -632,7 +632,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 			}
 			if len(missingFields) > 0 {
 				log.Error("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
-				if loginSource.IsOAuth2() && loginSource.Cfg.(*oauth2Service.Source).Provider == "openidConnect" {
+				if loginSource.IsOAuth2() && loginSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
 					log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
 				}
 				err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", loginSource.Name, missingFields)
@@ -773,7 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
 // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 // login the user
 func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
-	gothUser, err := loginSource.Cfg.(*oauth2Service.Source).ProviderCallback(loginSource, request, response)
+	gothUser, err := loginSource.Cfg.(*oauth2.Source).ProviderCallback(loginSource, request, response)
 	if err != nil {
 		if err.Error() == "securecookie: the value is too long" {
 			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
diff --git a/services/auth/source/oauth2/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
index fa775d9d3d539..c08cdce1ecdc9 100644
--- a/services/auth/source/oauth2/source_authenticate.go
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -12,8 +12,8 @@ import (
 	"github.com/markbates/goth/gothic"
 )
 
-// Authenticate takes a provided loginSource and the request/response pair to authenticate against the provider
-func (source *Source) Authenticate(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) error {
+// ProviderAuthenticate takes a provided loginSource and the request/response pair to authenticate against the provider
+func (source *Source) ProviderAuthenticate(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) error {
 	// not sure if goth is thread safe (?) when using multiple providers
 	request.Header.Set(ProviderHeaderKey, loginSource.Name)
 

From 9b36de83920cb21201bdfc0bb631dbb7d8b5e6be Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 17:59:33 +0100
Subject: [PATCH 18/44] minor cleanups

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go                        | 29 ++++++++++---------
 .../auth/source/oauth2/source_register.go     |  6 ++--
 2 files changed, 19 insertions(+), 16 deletions(-)

diff --git a/models/login_source.go b/models/login_source.go
index fa5b23fbedc3d..69d90849814a5 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -78,11 +78,22 @@ type RegisterableSource interface {
 }
 
 // RegisterLoginTypeConfig register a config for a provided type
-func RegisterLoginTypeConfig(typ LoginType, config LoginConfig) {
-	registeredLoginConfigs[typ] = config
+func RegisterLoginTypeConfig(typ LoginType, exemplar LoginConfig) {
+	if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
+		// Pointer:
+		registeredLoginConfigs[typ] = func() LoginConfig {
+			return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(LoginConfig)
+		}
+		return
+	}
+
+	// Not a Pointer
+	registeredLoginConfigs[typ] = func() LoginConfig {
+		return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(LoginConfig)
+	}
 }
 
-var registeredLoginConfigs = map[LoginType]LoginConfig{}
+var registeredLoginConfigs = map[LoginType]func() LoginConfig{}
 
 // LoginSource represents an external way for authorizing users.
 type LoginSource struct {
@@ -114,19 +125,11 @@ func Cell2Int64(val xorm.Cell) int64 {
 func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	if colName == "type" {
 		typ := LoginType(Cell2Int64(val))
-		exemplar, ok := registeredLoginConfigs[typ]
+		constructor, ok := registeredLoginConfigs[typ]
 		if !ok {
 			return
 		}
-
-		if reflect.TypeOf(exemplar).Kind() == reflect.Ptr {
-			// Pointer:
-			source.Cfg = reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(convert.Conversion)
-			return
-		}
-
-		// Not pointer:
-		source.Cfg = reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(convert.Conversion)
+		source.Cfg = constructor()
 	}
 }
 
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
index 6df1abc63cb96..0d346a51ae929 100644
--- a/services/auth/source/oauth2/source_register.go
+++ b/services/auth/source/oauth2/source_register.go
@@ -22,9 +22,9 @@ func (source *Source) UnregisterSource(loginSource *models.LoginSource) error {
 
 // wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2
 // inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models
-func wrapOpenIDConnectInitializeError(err error, providerName string, oAuth2Config *Source) error {
-	if err != nil && oAuth2Config.Provider == "openidConnect" {
-		err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: oAuth2Config.OpenIDConnectAutoDiscoveryURL, Cause: err}
+func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error {
+	if err != nil && source.Provider == "openidConnect" {
+		err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
 	}
 	return err
 }

From 8173d671f92cb53d0d15579dd461a78947270199 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 20 Jun 2021 19:53:42 +0100
Subject: [PATCH 19/44] Simplify login source functions

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go                        | 18 +++++---
 routers/web/user/auth.go                      |  6 +--
 services/auth/interface.go                    |  4 +-
 services/auth/signin.go                       |  4 +-
 .../auth/source/ldap/assert_interface_test.go | 24 +++++++++++
 services/auth/source/ldap/source.go           |  8 ++++
 .../auth/source/ldap/source_authenticate.go   | 12 +++---
 services/auth/source/ldap/source_sync.go      | 42 +++++++++----------
 .../source/oauth2/assert_interface_test.go    | 18 ++++++++
 services/auth/source/oauth2/init.go           |  2 +-
 services/auth/source/oauth2/source.go         |  8 ++++
 .../auth/source/oauth2/source_authenticate.go | 11 +++--
 .../auth/source/oauth2/source_register.go     | 10 ++---
 .../auth/source/pam/assert_interface_test.go  | 19 +++++++++
 services/auth/source/pam/source.go            |  8 ++++
 .../auth/source/pam/source_authenticate.go    |  4 +-
 .../auth/source/smtp/assert_interface_test.go | 22 ++++++++++
 services/auth/source/smtp/source.go           |  8 ++++
 .../auth/source/smtp/source_authenticate.go   |  4 +-
 .../auth/source/sspi/assert_interface_test.go | 16 +++++++
 services/auth/sync.go                         |  2 +-
 21 files changed, 194 insertions(+), 56 deletions(-)
 create mode 100644 services/auth/source/ldap/assert_interface_test.go
 create mode 100644 services/auth/source/oauth2/assert_interface_test.go
 create mode 100644 services/auth/source/pam/assert_interface_test.go
 create mode 100644 services/auth/source/smtp/assert_interface_test.go
 create mode 100644 services/auth/source/sspi/assert_interface_test.go

diff --git a/models/login_source.go b/models/login_source.go
index 69d90849814a5..c8dd160fcaa84 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -73,8 +73,13 @@ type SSHKeyProvider interface {
 
 // RegisterableSource configurations provide RegisterSource which needs to be run on creation
 type RegisterableSource interface {
-	RegisterSource(*LoginSource) error
-	UnregisterSource(*LoginSource) error
+	RegisterSource() error
+	UnregisterSource() error
+}
+
+// LoginSourceSettable configurations can have their loginSource set on them
+type LoginSourceSettable interface {
+	SetLoginSource(*LoginSource)
 }
 
 // RegisterLoginTypeConfig register a config for a provided type
@@ -130,6 +135,9 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 			return
 		}
 		source.Cfg = constructor()
+		if settable, ok := source.Cfg.(LoginSourceSettable); ok {
+			settable.SetLoginSource(source)
+		}
 	}
 }
 
@@ -215,7 +223,7 @@ func CreateLoginSource(source *LoginSource) error {
 		return nil
 	}
 
-	err = registerableSource.RegisterSource(source)
+	err = registerableSource.RegisterSource()
 	if err != nil {
 		// remove the LoginSource in case of errors while registering configuration
 		if _, err := x.Delete(source); err != nil {
@@ -307,7 +315,7 @@ func UpdateSource(source *LoginSource) error {
 		return nil
 	}
 
-	err = registerableSource.RegisterSource(source)
+	err = registerableSource.RegisterSource()
 	if err != nil {
 		// restore original values since we cannot update the provider it self
 		if _, err := x.ID(source.ID).AllCols().Update(originalLoginSource); err != nil {
@@ -334,7 +342,7 @@ func DeleteSource(source *LoginSource) error {
 	}
 
 	if registerableSource, ok := source.Cfg.(RegisterableSource); ok {
-		if err := registerableSource.UnregisterSource(source); err != nil {
+		if err := registerableSource.UnregisterSource(); err != nil {
 			return err
 		}
 	}
diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index 8210dc280a7c5..ebe5128754b57 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -578,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
 		return
 	}
 
-	if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
+	if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(ctx.Req, ctx.Resp); err != nil {
 		if strings.Contains(err.Error(), "no provider for ") {
 			if err = oauth2.ResetOAuth2(); err != nil {
 				ctx.ServerError("SignIn", err)
 				return
 			}
-			if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(loginSource, ctx.Req, ctx.Resp); err != nil {
+			if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(ctx.Req, ctx.Resp); err != nil {
 				ctx.ServerError("SignIn", err)
 			}
 			return
@@ -773,7 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
 // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 // login the user
 func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
-	gothUser, err := loginSource.Cfg.(*oauth2.Source).ProviderCallback(loginSource, request, response)
+	gothUser, err := loginSource.Cfg.(*oauth2.Source).ProviderCallback(request, response)
 	if err != nil {
 		if err.Error() == "securecookie: the value is too long" {
 			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
diff --git a/services/auth/interface.go b/services/auth/interface.go
index 41a28d1a7119a..44effb1d0ae0f 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -41,10 +41,10 @@ type Method interface {
 
 // PasswordAuthenticator represents a source of authentication
 type PasswordAuthenticator interface {
-	Authenticate(user *models.User, login, password string, source *models.LoginSource) (*models.User, error)
+	Authenticate(user *models.User, login, password string) (*models.User, error)
 }
 
 // SynchronizableSource represents a source that can synchronize users
 type SynchronizableSource interface {
-	Sync(ctx context.Context, updateExisting bool, source *models.LoginSource) error
+	Sync(ctx context.Context, updateExisting bool) error
 }
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 521b7c01d4dad..3a78136786805 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -68,7 +68,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 
 			}
 
-			user, err := authenticator.Authenticate(nil, username, password, source)
+			user, err := authenticator.Authenticate(nil, username, password)
 			if err != nil {
 				return nil, err
 			}
@@ -99,7 +99,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 			continue
 		}
 
-		authUser, err := authenticator.Authenticate(nil, username, password, source)
+		authUser, err := authenticator.Authenticate(nil, username, password)
 
 		if err == nil {
 			if !user.ProhibitLogin {
diff --git a/services/auth/source/ldap/assert_interface_test.go b/services/auth/source/ldap/assert_interface_test.go
new file mode 100644
index 0000000000000..2b57ca0fa733b
--- /dev/null
+++ b/services/auth/source/ldap/assert_interface_test.go
@@ -0,0 +1,24 @@
+// Copyright 2021 The Gitea 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_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/ldap"
+)
+
+type sourceInterface interface {
+	auth.PasswordAuthenticator
+	auth.SynchronizableSource
+	models.SSHKeyProvider
+	models.LoginConfig
+	models.SkipVerifiable
+	models.HasTLSer
+	models.UseTLSer
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &ldap.Source{}
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index 4fd218cbe949c..80ed3e75e6e51 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -53,6 +53,9 @@ type Source struct {
 	GroupFilter           string // Group Name Filter
 	GroupMemberUID        string // Group Attribute containing array of UserUID
 	UserUID               string // User Attribute listed in Group
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
 }
 
 // wrappedSource wraps the source to ensure that the FromDB/ToDB results are the same as previously
@@ -120,6 +123,11 @@ func (source *Source) ProvidesSSHKeys() bool {
 	return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 }
 
+// SetLoginSource sets the related LoginSource
+func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
+	source.loginSource = loginSource
+}
+
 func init() {
 	models.RegisterLoginTypeConfig(models.LoginLDAP, &Source{})
 	models.RegisterLoginTypeConfig(models.LoginDLDAP, &Source{})
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 29325c2e83226..1d5e69539bc21 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -13,8 +13,8 @@ import (
 
 // Authenticate queries if login/password is valid against the LDAP directory pool,
 // and create a local user if success when enabled.
-func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
-	sr := source.SearchEntry(login, password, loginSource.Type == models.LoginDLDAP)
+func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
+	sr := source.SearchEntry(login, password, source.loginSource.Type == models.LoginDLDAP)
 	if sr == nil {
 		// User not in LDAP, do nothing
 		return nil, models.ErrUserNotExist{Name: login}
@@ -54,7 +54,7 @@ func (source *Source) Authenticate(user *models.User, login, password string, lo
 	}
 
 	if user != nil {
-		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, loginSource, sr.SSHPublicKey) {
+		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source.loginSource, sr.SSHPublicKey) {
 			return user, models.RewriteAllPublicKeys()
 		}
 
@@ -75,8 +75,8 @@ func (source *Source) Authenticate(user *models.User, login, password string, lo
 		Name:         sr.Username,
 		FullName:     composeFullName(sr.Name, sr.Surname, sr.Username),
 		Email:        sr.Mail,
-		LoginType:    loginSource.Type,
-		LoginSource:  loginSource.ID,
+		LoginType:    source.loginSource.Type,
+		LoginSource:  source.loginSource.ID,
 		LoginName:    login,
 		IsActive:     true,
 		IsAdmin:      sr.IsAdmin,
@@ -85,7 +85,7 @@ func (source *Source) Authenticate(user *models.User, login, password string, lo
 
 	err := models.CreateUser(user)
 
-	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, loginSource, sr.SSHPublicKey) {
+	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
 		err = models.RewriteAllPublicKeys()
 	}
 
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index a91269bc09ea4..7e4088e571738 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -14,29 +14,29 @@ import (
 )
 
 // Sync causes this ldap source to synchronize its users with the db
-func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.LoginSource) error {
-	log.Trace("Doing: SyncExternalUsers[%s]", s.Name)
+func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
+	log.Trace("Doing: SyncExternalUsers[%s]", source.loginSource.Name)
 
 	var existingUsers []int64
 	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
 	var sshKeysNeedUpdate bool
 
 	// Find all users with this login type - FIXME: Should this be an iterator?
-	users, err := models.GetUsersBySource(s)
+	users, err := models.GetUsersBySource(source.loginSource)
 	if err != nil {
 		log.Error("SyncExternalUsers: %v", err)
 		return err
 	}
 	select {
 	case <-ctx.Done():
-		log.Warn("SyncExternalUsers: Cancelled before update of %s", s.Name)
-		return models.ErrCancelledf("Before update of %s", s.Name)
+		log.Warn("SyncExternalUsers: Cancelled before update of %s", source.loginSource.Name)
+		return models.ErrCancelledf("Before update of %s", source.loginSource.Name)
 	default:
 	}
 
 	sr, err := source.SearchEntries()
 	if err != nil {
-		log.Error("SyncExternalUsers LDAP source failure [%s], skipped", s.Name)
+		log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.loginSource.Name)
 		return nil
 	}
 
@@ -51,7 +51,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 	for _, su := range sr {
 		select {
 		case <-ctx.Done():
-			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", s.Name)
+			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.loginSource.Name)
 			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 			if sshKeysNeedUpdate {
 				err = models.RewriteAllPublicKeys()
@@ -59,7 +59,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 					log.Error("RewriteAllPublicKeys: %v", err)
 				}
 			}
-			return models.ErrCancelledf("During update of %s before completed update of users", s.Name)
+			return models.ErrCancelledf("During update of %s before completed update of users", source.loginSource.Name)
 		default:
 		}
 		if len(su.Username) == 0 {
@@ -82,14 +82,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 		fullName := composeFullName(su.Name, su.Surname, su.Username)
 		// If no existing user found, create one
 		if usr == nil {
-			log.Trace("SyncExternalUsers[%s]: Creating user %s", s.Name, su.Username)
+			log.Trace("SyncExternalUsers[%s]: Creating user %s", source.loginSource.Name, su.Username)
 
 			usr = &models.User{
 				LowerName:    strings.ToLower(su.Username),
 				Name:         su.Username,
 				FullName:     fullName,
-				LoginType:    s.Type,
-				LoginSource:  s.ID,
+				LoginType:    source.loginSource.Type,
+				LoginSource:  source.loginSource.ID,
 				LoginName:    su.Username,
 				Email:        su.Mail,
 				IsAdmin:      su.IsAdmin,
@@ -100,10 +100,10 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 			err = models.CreateUser(usr)
 
 			if err != nil {
-				log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", s.Name, su.Username, err)
+				log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.loginSource.Name, su.Username, err)
 			} else if isAttributeSSHPublicKeySet {
-				log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", s.Name, usr.Name)
-				if models.AddPublicKeysBySource(usr, s, su.SSHPublicKey) {
+				log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.loginSource.Name, usr.Name)
+				if models.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
 					sshKeysNeedUpdate = true
 				}
 			}
@@ -111,7 +111,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 			existingUsers = append(existingUsers, usr.ID)
 
 			// Synchronize SSH Public Key if that attribute is set
-			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, s, su.SSHPublicKey) {
+			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
 				sshKeysNeedUpdate = true
 			}
 
@@ -122,7 +122,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 				usr.FullName != fullName ||
 				!usr.IsActive {
 
-				log.Trace("SyncExternalUsers[%s]: Updating user %s", s.Name, usr.Name)
+				log.Trace("SyncExternalUsers[%s]: Updating user %s", source.loginSource.Name, usr.Name)
 
 				usr.FullName = fullName
 				usr.Email = su.Mail
@@ -138,7 +138,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 
 				err = models.UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active")
 				if err != nil {
-					log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", s.Name, usr.Name, err)
+					log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.loginSource.Name, usr.Name, err)
 				}
 			}
 		}
@@ -154,8 +154,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 
 	select {
 	case <-ctx.Done():
-		log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", s.Name)
-		return models.ErrCancelledf("During update of %s before delete users", s.Name)
+		log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.loginSource.Name)
+		return models.ErrCancelledf("During update of %s before delete users", source.loginSource.Name)
 	default:
 	}
 
@@ -170,12 +170,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool, s *models.L
 				}
 			}
 			if !found {
-				log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name)
+				log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.loginSource.Name, usr.Name)
 
 				usr.IsActive = false
 				err = models.UpdateUserCols(usr, "is_active")
 				if err != nil {
-					log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err)
+					log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.loginSource.Name, usr.Name, err)
 				}
 			}
 		}
diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
new file mode 100644
index 0000000000000..3ad18dbdb791a
--- /dev/null
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -0,0 +1,18 @@
+// Copyright 2021 The Gitea 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 oauth2_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
+)
+
+type sourceInterface interface {
+	models.LoginConfig
+	models.LoginSourceSettable
+	models.RegisterableSource
+}
+
+var _ (sourceInterface) = &oauth2.Source{}
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index 15d16a623c80a..f602e6725afb3 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -69,7 +69,7 @@ func initOAuth2LoginSources() error {
 		if !ok {
 			continue
 		}
-		err := oauth2Source.RegisterSource(source)
+		err := oauth2Source.RegisterSource()
 		if err != nil {
 			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
 			source.IsActived = false
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 3a5c221c366d7..7780b5cc42638 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -25,6 +25,9 @@ type Source struct {
 	OpenIDConnectAutoDiscoveryURL string
 	CustomURLMapping              *CustomURLMapping
 	IconURL                       string
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
 }
 
 // FromDB fills up an OAuth2Config from serialized format.
@@ -39,6 +42,11 @@ func (source *Source) ToDB() ([]byte, error) {
 	return json.Marshal(source)
 }
 
+// SetLoginSource sets the related LoginSource
+func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
+	source.loginSource = loginSource
+}
+
 func init() {
 	models.RegisterLoginTypeConfig(models.LoginOAuth2, &Source{})
 }
diff --git a/services/auth/source/oauth2/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
index c08cdce1ecdc9..7f56a922a3f14 100644
--- a/services/auth/source/oauth2/source_authenticate.go
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -7,15 +7,14 @@ package oauth2
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	"github.com/markbates/goth"
 	"github.com/markbates/goth/gothic"
 )
 
-// ProviderAuthenticate takes a provided loginSource and the request/response pair to authenticate against the provider
-func (source *Source) ProviderAuthenticate(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) error {
+// ProviderAuthenticate redirects request/response pair to authenticate against the provider
+func (source *Source) ProviderAuthenticate(request *http.Request, response http.ResponseWriter) error {
 	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(ProviderHeaderKey, loginSource.Name)
+	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
 
 	// don't use the default gothic begin handler to prevent issues when some error occurs
 	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
@@ -30,9 +29,9 @@ func (source *Source) ProviderAuthenticate(loginSource *models.LoginSource, requ
 
 // ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
 // this will trigger a new authentication request, but because we save it in the session we can use that
-func (source *Source) ProviderCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (goth.User, error) {
+func (source *Source) ProviderCallback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
 	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(ProviderHeaderKey, loginSource.Name)
+	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
 
 	user, err := gothic.CompleteUserAuth(response, request)
 	if err != nil {
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
index 0d346a51ae929..b61cc3fe7948f 100644
--- a/services/auth/source/oauth2/source_register.go
+++ b/services/auth/source/oauth2/source_register.go
@@ -9,14 +9,14 @@ import (
 )
 
 // RegisterSource causes an OAuth2 configuration to be registered
-func (source *Source) RegisterSource(loginSource *models.LoginSource) error {
-	err := RegisterProvider(loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
-	return wrapOpenIDConnectInitializeError(err, loginSource.Name, source)
+func (source *Source) RegisterSource() error {
+	err := RegisterProvider(source.loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping)
+	return wrapOpenIDConnectInitializeError(err, source.loginSource.Name, source)
 }
 
 // UnregisterSource causes an OAuth2 configuration to be unregistered
-func (source *Source) UnregisterSource(loginSource *models.LoginSource) error {
-	RemoveProvider(loginSource.Name)
+func (source *Source) UnregisterSource() error {
+	RemoveProvider(source.loginSource.Name)
 	return nil
 }
 
diff --git a/services/auth/source/pam/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go
new file mode 100644
index 0000000000000..e4371d282d570
--- /dev/null
+++ b/services/auth/source/pam/assert_interface_test.go
@@ -0,0 +1,19 @@
+// Copyright 2021 The Gitea 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_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/pam"
+)
+
+type sourceInterface interface {
+	auth.PasswordAuthenticator
+	models.LoginConfig
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &pam.Source{}
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index f47ea9fb18d07..124d61974af97 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -21,6 +21,9 @@ import (
 type Source struct {
 	ServiceName string // pam service (e.g. system-auth)
 	EmailDomain string
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
 }
 
 // FromDB fills up a PAMConfig from serialized format.
@@ -35,6 +38,11 @@ func (source *Source) ToDB() ([]byte, error) {
 	return json.Marshal(source)
 }
 
+// SetLoginSource sets the related LoginSource
+func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
+	source.loginSource = loginSource
+}
+
 func init() {
 	models.RegisterLoginTypeConfig(models.LoginPAM, &Source{})
 }
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index ae8ce1fba44f5..6ca06429041cf 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -17,7 +17,7 @@ import (
 
 // Authenticate queries if login/password is valid against the PAM,
 // and create a local user if success when enabled.
-func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
+func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 	pamLogin, err := pam.Auth(source.ServiceName, login, password)
 	if err != nil {
 		if strings.Contains(err.Error(), "Authentication failure") {
@@ -54,7 +54,7 @@ func (source *Source) Authenticate(user *models.User, login, password string, lo
 		Email:       email,
 		Passwd:      password,
 		LoginType:   models.LoginPAM,
-		LoginSource: loginSource.ID,
+		LoginSource: source.loginSource.ID,
 		LoginName:   login, // This is what the user typed in
 		IsActive:    true,
 	}
diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go
new file mode 100644
index 0000000000000..261d0108f57c1
--- /dev/null
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -0,0 +1,22 @@
+// Copyright 2021 The Gitea 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_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/smtp"
+)
+
+type sourceInterface interface {
+	auth.PasswordAuthenticator
+	models.LoginConfig
+	models.SkipVerifiable
+	models.HasTLSer
+	models.UseTLSer
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &smtp.Source{}
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index b3aabe6f2793c..104322dedc608 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -25,6 +25,9 @@ type Source struct {
 	AllowedDomains string `xorm:"TEXT"`
 	TLS            bool
 	SkipVerify     bool
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
 }
 
 // FromDB fills up an SMTPConfig from serialized format.
@@ -54,6 +57,11 @@ func (source *Source) UseTLS() bool {
 	return source.TLS
 }
 
+// SetLoginSource sets the related LoginSource
+func (source *Source) SetLoginSource(loginSource *models.LoginSource) {
+	source.loginSource = loginSource
+}
+
 func init() {
 	models.RegisterLoginTypeConfig(models.LoginSMTP, &Source{})
 }
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index b29525d3f53c4..9bab86604bc7b 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -16,7 +16,7 @@ import (
 
 // Authenticate queries if the provided login/password is authenticates against the SMTP server
 // Users will be autoregistered as required
-func (source *Source) Authenticate(user *models.User, login, password string, loginSource *models.LoginSource) (*models.User, error) {
+func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
 	// Verify allowed domains.
 	if len(source.AllowedDomains) > 0 {
 		idx := strings.Index(login, "@")
@@ -63,7 +63,7 @@ func (source *Source) Authenticate(user *models.User, login, password string, lo
 		Email:       login,
 		Passwd:      password,
 		LoginType:   models.LoginSMTP,
-		LoginSource: loginSource.ID,
+		LoginSource: source.loginSource.ID,
 		LoginName:   login,
 		IsActive:    true,
 	}
diff --git a/services/auth/source/sspi/assert_interface_test.go b/services/auth/source/sspi/assert_interface_test.go
new file mode 100644
index 0000000000000..a958a3a4d31f1
--- /dev/null
+++ b/services/auth/source/sspi/assert_interface_test.go
@@ -0,0 +1,16 @@
+// Copyright 2021 The Gitea 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 sspi_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth/source/sspi"
+)
+
+type sourceInterface interface {
+	models.LoginConfig
+}
+
+var _ (sourceInterface) = &sspi.Source{}
diff --git a/services/auth/sync.go b/services/auth/sync.go
index a976270464681..a608787aad891 100644
--- a/services/auth/sync.go
+++ b/services/auth/sync.go
@@ -33,7 +33,7 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 		}
 
 		if syncable, ok := s.Cfg.(SynchronizableSource); ok {
-			err := syncable.Sync(ctx, updateExisting, s)
+			err := syncable.Sync(ctx, updateExisting)
 			if err != nil {
 				return err
 			}

From 3747caced767aefe96d4fc21937ddba169726278 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:13:57 +0100
Subject: [PATCH 20/44] Despecialize db authentication and fix bug

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 routers/web/user/auth.go                      |  6 +--
 services/auth/signin.go                       | 52 ++++++++-----------
 .../auth/source/db/assert_interface_test.go   | 18 +++++++
 services/auth/source/db/authenticate.go       |  4 ++
 services/auth/source/db/source.go             | 30 +++++++++++
 .../source/oauth2/assert_interface_test.go    |  2 +
 .../auth/source/oauth2/source_authenticate.go | 37 ++-----------
 services/auth/source/oauth2/source_callout.go | 42 +++++++++++++++
 8 files changed, 127 insertions(+), 64 deletions(-)
 create mode 100644 services/auth/source/db/assert_interface_test.go
 create mode 100644 services/auth/source/db/source.go
 create mode 100644 services/auth/source/oauth2/source_callout.go

diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go
index ebe5128754b57..eb0d05072d56d 100644
--- a/routers/web/user/auth.go
+++ b/routers/web/user/auth.go
@@ -578,13 +578,13 @@ func SignInOAuth(ctx *context.Context) {
 		return
 	}
 
-	if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(ctx.Req, ctx.Resp); err != nil {
+	if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 		if strings.Contains(err.Error(), "no provider for ") {
 			if err = oauth2.ResetOAuth2(); err != nil {
 				ctx.ServerError("SignIn", err)
 				return
 			}
-			if err = loginSource.Cfg.(*oauth2.Source).ProviderAuthenticate(ctx.Req, ctx.Resp); err != nil {
+			if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 				ctx.ServerError("SignIn", err)
 			}
 			return
@@ -773,7 +773,7 @@ func handleOAuth2SignIn(ctx *context.Context, u *models.User, gothUser goth.User
 // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
 // login the user
 func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
-	gothUser, err := loginSource.Cfg.(*oauth2.Source).ProviderCallback(request, response)
+	gothUser, err := loginSource.Cfg.(*oauth2.Source).Callback(request, response)
 	if err != nil {
 		if err.Error() == "securecookie: the value is too long" {
 			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 3a78136786805..a0430274e06dc 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -9,9 +9,9 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/services/auth/source/db"
 
-	// Register the other sources
+	// Register the sources
+	_ "code.gitea.io/gitea/services/auth/source/db"
 	_ "code.gitea.io/gitea/services/auth/source/ldap"
 	_ "code.gitea.io/gitea/services/auth/source/oauth2"
 	_ "code.gitea.io/gitea/services/auth/source/pam"
@@ -49,38 +49,32 @@ func UserSignIn(username, password string) (*models.User, error) {
 	}
 
 	if hasUser {
-		switch user.LoginType {
-		case models.LoginNoType, models.LoginPlain, models.LoginOAuth2:
-			return db.Authenticate(user, user.Name, password)
-		default:
-			source, err := models.GetLoginSourceByID(user.LoginSource)
-			if err != nil {
-				return nil, err
-			}
-
-			if !source.IsActived {
-				return nil, models.ErrLoginSourceNotActived
-			}
-
-			authenticator, ok := source.Cfg.(PasswordAuthenticator)
-			if !ok {
-				return nil, models.ErrUnsupportedLoginType
+		source, err := models.GetLoginSourceByID(user.LoginSource)
+		if err != nil {
+			return nil, err
+		}
 
-			}
+		if !source.IsActived {
+			return nil, models.ErrLoginSourceNotActived
+		}
 
-			user, err := authenticator.Authenticate(nil, username, password)
-			if err != nil {
-				return nil, err
-			}
+		authenticator, ok := source.Cfg.(PasswordAuthenticator)
+		if !ok {
+			return nil, models.ErrUnsupportedLoginType
+		}
 
-			// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
-			// user could be hint to resend confirm email.
-			if user.ProhibitLogin {
-				return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
-			}
+		user, err := authenticator.Authenticate(user, username, password)
+		if err != nil {
+			return nil, err
+		}
 
-			return user, nil
+		// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
+		// user could be hint to resend confirm email.
+		if user.ProhibitLogin {
+			return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
 		}
+
+		return user, nil
 	}
 
 	sources, err := models.ActiveLoginSources(-1)
diff --git a/services/auth/source/db/assert_interface_test.go b/services/auth/source/db/assert_interface_test.go
new file mode 100644
index 0000000000000..09346669c944c
--- /dev/null
+++ b/services/auth/source/db/assert_interface_test.go
@@ -0,0 +1,18 @@
+// Copyright 2021 The Gitea 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 db_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"code.gitea.io/gitea/services/auth/source/db"
+)
+
+type sourceInterface interface {
+	auth.PasswordAuthenticator
+	models.LoginConfig
+}
+
+var _ (sourceInterface) = &db.Source{}
diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go
index dc85fb2e89e57..e73ab15d2835e 100644
--- a/services/auth/source/db/authenticate.go
+++ b/services/auth/source/db/authenticate.go
@@ -11,6 +11,10 @@ import (
 
 // Authenticate authenticates the provided user against the DB
 func Authenticate(user *models.User, login, password string) (*models.User, error) {
+	if user == nil {
+		return nil, models.ErrUserNotExist{Name: login}
+	}
+
 	if !user.IsPasswordSet() || !user.ValidatePassword(password) {
 		return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
 	}
diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
new file mode 100644
index 0000000000000..1d7942cbc966f
--- /dev/null
+++ b/services/auth/source/db/source.go
@@ -0,0 +1,30 @@
+// Copyright 2021 The Gitea 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 db
+
+import "code.gitea.io/gitea/models"
+
+type Source struct{}
+
+// FromDB fills up an OAuth2Config from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	return nil
+}
+
+// ToDB exports an SMTPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	return nil, nil
+}
+
+// Authenticate queries if login/password is valid against the PAM,
+// and create a local user if success when enabled.
+func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
+	return Authenticate(user, login, password)
+}
+
+func init() {
+	models.RegisterLoginTypeConfig(models.LoginNoType, &Source{})
+	models.RegisterLoginTypeConfig(models.LoginPlain, &Source{})
+}
diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
index 3ad18dbdb791a..b527ec9881bc8 100644
--- a/services/auth/source/oauth2/assert_interface_test.go
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -6,6 +6,7 @@ package oauth2_test
 
 import (
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 )
 
@@ -13,6 +14,7 @@ type sourceInterface interface {
 	models.LoginConfig
 	models.LoginSourceSettable
 	models.RegisterableSource
+	auth.PasswordAuthenticator
 }
 
 var _ (sourceInterface) = &oauth2.Source{}
diff --git a/services/auth/source/oauth2/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
index 7f56a922a3f14..2e39f245dff07 100644
--- a/services/auth/source/oauth2/source_authenticate.go
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -5,38 +5,11 @@
 package oauth2
 
 import (
-	"net/http"
-
-	"github.com/markbates/goth"
-	"github.com/markbates/goth/gothic"
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth/source/db"
 )
 
-// ProviderAuthenticate redirects request/response pair to authenticate against the provider
-func (source *Source) ProviderAuthenticate(request *http.Request, response http.ResponseWriter) error {
-	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
-
-	// don't use the default gothic begin handler to prevent issues when some error occurs
-	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
-	//gothic.BeginAuthHandler(response, request)
-
-	url, err := gothic.GetAuthURL(response, request)
-	if err == nil {
-		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
-	}
-	return err
-}
-
-// ProviderCallback handles OAuth callback, resolve to a goth user and send back to original url
-// this will trigger a new authentication request, but because we save it in the session we can use that
-func (source *Source) ProviderCallback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
-	// not sure if goth is thread safe (?) when using multiple providers
-	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
-
-	user, err := gothic.CompleteUserAuth(response, request)
-	if err != nil {
-		return user, err
-	}
-
-	return user, nil
+// Authenticate falls back to the db authenticator
+func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
+	return db.Authenticate(user, login, password)
 }
diff --git a/services/auth/source/oauth2/source_callout.go b/services/auth/source/oauth2/source_callout.go
new file mode 100644
index 0000000000000..8f4663f3bea12
--- /dev/null
+++ b/services/auth/source/oauth2/source_callout.go
@@ -0,0 +1,42 @@
+// Copyright 2021 The Gitea 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 oauth2
+
+import (
+	"net/http"
+
+	"github.com/markbates/goth"
+	"github.com/markbates/goth/gothic"
+)
+
+// Callout redirects request/response pair to authenticate against the provider
+func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
+	// not sure if goth is thread safe (?) when using multiple providers
+	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
+
+	// don't use the default gothic begin handler to prevent issues when some error occurs
+	// normally the gothic library will write some custom stuff to the response instead of our own nice error page
+	//gothic.BeginAuthHandler(response, request)
+
+	url, err := gothic.GetAuthURL(response, request)
+	if err == nil {
+		http.Redirect(response, request, url, http.StatusTemporaryRedirect)
+	}
+	return err
+}
+
+// Callback handles OAuth callback, resolve to a goth user and send back to original url
+// this will trigger a new authentication request, but because we save it in the session we can use that
+func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
+	// not sure if goth is thread safe (?) when using multiple providers
+	request.Header.Set(ProviderHeaderKey, source.loginSource.Name)
+
+	user, err := gothic.CompleteUserAuth(response, request)
+	if err != nil {
+		return user, err
+	}
+
+	return user, nil
+}

From 550ed703ee456937b7aa4498c19980bf68772a4d Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:19:13 +0100
Subject: [PATCH 21/44] Add explanation to assert_interface_test.go

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/source/db/assert_interface_test.go     | 3 +++
 services/auth/source/ldap/assert_interface_test.go   | 3 +++
 services/auth/source/oauth2/assert_interface_test.go | 3 +++
 services/auth/source/pam/assert_interface_test.go    | 3 +++
 services/auth/source/smtp/assert_interface_test.go   | 3 +++
 services/auth/source/sspi/assert_interface_test.go   | 3 +++
 6 files changed, 18 insertions(+)

diff --git a/services/auth/source/db/assert_interface_test.go b/services/auth/source/db/assert_interface_test.go
index 09346669c944c..2e0fa9ba2247a 100644
--- a/services/auth/source/db/assert_interface_test.go
+++ b/services/auth/source/db/assert_interface_test.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/db"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	auth.PasswordAuthenticator
 	models.LoginConfig
diff --git a/services/auth/source/ldap/assert_interface_test.go b/services/auth/source/ldap/assert_interface_test.go
index 2b57ca0fa733b..4cf3eafe76358 100644
--- a/services/auth/source/ldap/assert_interface_test.go
+++ b/services/auth/source/ldap/assert_interface_test.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/ldap"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	auth.PasswordAuthenticator
 	auth.SynchronizableSource
diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
index b527ec9881bc8..4157427ff2f76 100644
--- a/services/auth/source/oauth2/assert_interface_test.go
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/oauth2"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	models.LoginConfig
 	models.LoginSourceSettable
diff --git a/services/auth/source/pam/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go
index e4371d282d570..a0bebdf9c6799 100644
--- a/services/auth/source/pam/assert_interface_test.go
+++ b/services/auth/source/pam/assert_interface_test.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/pam"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	auth.PasswordAuthenticator
 	models.LoginConfig
diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go
index 261d0108f57c1..bc2042e069965 100644
--- a/services/auth/source/smtp/assert_interface_test.go
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -10,6 +10,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/smtp"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	auth.PasswordAuthenticator
 	models.LoginConfig
diff --git a/services/auth/source/sspi/assert_interface_test.go b/services/auth/source/sspi/assert_interface_test.go
index a958a3a4d31f1..605a6ec6c541a 100644
--- a/services/auth/source/sspi/assert_interface_test.go
+++ b/services/auth/source/sspi/assert_interface_test.go
@@ -9,6 +9,9 @@ import (
 	"code.gitea.io/gitea/services/auth/source/sspi"
 )
 
+// This test file exists to assert that our Source exposes the interfaces that we expect
+// It tightly binds the interfaces and implementation without breaking go import cycles
+
 type sourceInterface interface {
 	models.LoginConfig
 }

From 8f1303775c42e35928da27408fb41a342f8babe9 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:21:02 +0100
Subject: [PATCH 22/44] fixup! Despecialize db authentication and fix bug

---
 models/login_source.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/models/login_source.go b/models/login_source.go
index c8dd160fcaa84..a1086c02224b4 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -281,6 +281,11 @@ func IsSSPIEnabled() bool {
 // GetLoginSourceByID returns login source by given ID.
 func GetLoginSourceByID(id int64) (*LoginSource, error) {
 	source := new(LoginSource)
+	if id == 0 {
+		source.Cfg = registeredLoginConfigs[LoginNoType]()
+		return source, nil
+	}
+
 	has, err := x.ID(id).Get(source)
 	if err != nil {
 		return nil, err

From 49c6c2a253e73442aada05967faed2a0d4783219 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:28:50 +0100
Subject: [PATCH 23/44] create AllActiveLoginSources as ActiveLoginSources(-1)
 is a bit weird

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go  | 16 +++++++++-------
 services/auth/signin.go |  2 +-
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/models/login_source.go b/models/login_source.go
index a1086c02224b4..da9189175b912 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -248,16 +248,18 @@ func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
 	return sources, nil
 }
 
+// AllActiveLoginSources returns all active sources
+func AllActiveLoginSources() ([]*LoginSource, error) {
+	sources := make([]*LoginSource, 0, 5)
+	if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
+		return nil, err
+	}
+	return sources, nil
+}
+
 // ActiveLoginSources returns all active sources of the specified type
 func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 1)
-	if loginType < 0 {
-		if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
-			return nil, err
-		}
-		return sources, nil
-	}
-
 	if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
 		return nil, err
 	}
diff --git a/services/auth/signin.go b/services/auth/signin.go
index a0430274e06dc..a527e02bca37f 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -77,7 +77,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 		return user, nil
 	}
 
-	sources, err := models.ActiveLoginSources(-1)
+	sources, err := models.AllActiveLoginSources()
 	if err != nil {
 		return nil, err
 	}

From c17fda3ab91d8bcee12f210236470658df9df059 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:32:52 +0100
Subject: [PATCH 24/44] Handle prohibit logins correctly

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/signin.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/services/auth/signin.go b/services/auth/signin.go
index a527e02bca37f..224688e8d1195 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -96,10 +96,10 @@ func UserSignIn(username, password string) (*models.User, error) {
 		authUser, err := authenticator.Authenticate(nil, username, password)
 
 		if err == nil {
-			if !user.ProhibitLogin {
+			if !authUser.ProhibitLogin {
 				return authUser, nil
 			}
-			err = models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
+			err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
 		}
 
 		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)

From cb668e5bf3b9d94edad6bbec5ed4f3583273b31c Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 04:35:20 +0100
Subject: [PATCH 25/44] fixup! Despecialize db authentication and fix bug

---
 services/auth/source/db/source.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index 1d7942cbc966f..182c05f0dfcc5 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -6,6 +6,7 @@ package db
 
 import "code.gitea.io/gitea/models"
 
+// Source is a password authentication service
 type Source struct{}
 
 // FromDB fills up an OAuth2Config from serialized format.

From 320136e428b446c0c8feb40343a1d9a9f5857e34 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 05:00:32 +0100
Subject: [PATCH 26/44] Simplify auth.Method interface

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/auth.go         | 14 ++++++++++++--
 services/auth/basic.go        | 11 +----------
 services/auth/group.go        | 30 +++++++++++++++++++-----------
 services/auth/interface.go    | 26 ++++++++++++++++++--------
 services/auth/oauth2.go       | 11 +----------
 services/auth/reverseproxy.go | 11 +----------
 services/auth/session.go      | 10 ----------
 services/auth/sspi_windows.go |  5 ++++-
 8 files changed, 56 insertions(+), 62 deletions(-)

diff --git a/services/auth/auth.go b/services/auth/auth.go
index 1dedfbf779b58..854abcacd1667 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -57,7 +57,12 @@ func Init() {
 	}
 	specialInit()
 	for _, method := range Methods() {
-		err := method.Init()
+		initalizable, ok := method.(Initializable)
+		if !ok {
+			continue
+		}
+
+		err := initalizable.Init()
 		if err != nil {
 			log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 		}
@@ -68,7 +73,12 @@ func Init() {
 // to release necessary resources
 func Free() {
 	for _, method := range Methods() {
-		err := method.Free()
+		freeable, ok := method.(Freeable)
+		if !ok {
+			continue
+		}
+
+		err := freeable.Free()
 		if err != nil {
 			log.Error("Could not free '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 		}
diff --git a/services/auth/basic.go b/services/auth/basic.go
index f0486f905c379..d492a52a6693f 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -20,6 +20,7 @@ import (
 // Ensure the struct implements the interface.
 var (
 	_ Method = &Basic{}
+	_ Named  = &Basic{}
 )
 
 // Basic implements the Auth interface and authenticates requests (API requests
@@ -33,16 +34,6 @@ func (b *Basic) Name() string {
 	return "basic"
 }
 
-// Init does nothing as the Basic implementation does not need to allocate any resources
-func (b *Basic) Init() error {
-	return nil
-}
-
-// Free does nothing as the Basic implementation does not have to release any resources
-func (b *Basic) Free() error {
-	return nil
-}
-
 // Verify extracts and validates Basic data (username and password/token) from the
 // "Authorization" header of the request and returns the corresponding user object for that
 // name/token on successful validation.
diff --git a/services/auth/group.go b/services/auth/group.go
index 7e887cfa8b58e..fb885b818ab4d 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -12,7 +12,9 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Method = &Group{}
+	_ Method        = &Group{}
+	_ Initializable = &Group{}
+	_ Freeable      = &Group{}
 )
 
 // Group implements the Auth interface with serval Auth.
@@ -27,15 +29,15 @@ func NewGroup(methods ...Method) *Group {
 	}
 }
 
-// Name represents the name of auth method
-func (b *Group) Name() string {
-	return "group"
-}
-
 // Init does nothing as the Basic implementation does not need to allocate any resources
 func (b *Group) Init() error {
-	for _, m := range b.methods {
-		if err := m.Init(); err != nil {
+	for _, method := range b.methods {
+		initializable, ok := method.(Initializable)
+		if !ok {
+			continue
+		}
+
+		if err := initializable.Init(); err != nil {
 			return err
 		}
 	}
@@ -44,8 +46,12 @@ func (b *Group) Init() error {
 
 // Free does nothing as the Basic implementation does not have to release any resources
 func (b *Group) Free() error {
-	for _, m := range b.methods {
-		if err := m.Free(); err != nil {
+	for _, method := range b.methods {
+		freeable, ok := method.(Freeable)
+		if !ok {
+			continue
+		}
+		if err := freeable.Free(); err != nil {
 			return err
 		}
 	}
@@ -63,7 +69,9 @@ func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore
 		user := ssoMethod.Verify(req, w, store, sess)
 		if user != nil {
 			if store.GetData()["AuthedMethod"] == nil {
-				store.GetData()["AuthedMethod"] = ssoMethod.Name()
+				if named, ok := ssoMethod.(Named); ok {
+					store.GetData()["AuthedMethod"] = named.Name()
+				}
 			}
 			return user
 		}
diff --git a/services/auth/interface.go b/services/auth/interface.go
index 44effb1d0ae0f..51c7043370bb1 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -21,22 +21,32 @@ type SessionStore session.Store
 
 // Method represents an authentication method (plugin) for HTTP requests.
 type Method interface {
-	Name() string
+	// Verify tries to verify the authentication data contained in the request.
+	// If verification is successful returns either an existing user object (with id > 0)
+	// or a new user object (with id = 0) populated with the information that was found
+	// in the authentication data (username or email).
+	// Returns nil if verification fails.
+	Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
+}
 
+// Initializable represents a structure that requires initialization
+// It usually should only be called once before anything else is called
+type Initializable interface {
 	// Init should be called exactly once before using any of the other methods,
 	// in order to allow the plugin to allocate necessary resources
 	Init() error
+}
 
+// Named represents a named thing
+type Named interface {
+	Name() string
+}
+
+// Freeable represents a structure that is required to be freed
+type Freeable interface {
 	// Free should be called exactly once before application closes, in order to
 	// give chance to the plugin to free any allocated resources
 	Free() error
-
-	// Verify tries to verify the authentication data contained in the request.
-	// If verification is successful returns either an existing user object (with id > 0)
-	// or a new user object (with id = 0) populated with the information that was found
-	// in the authentication data (username or email).
-	// Returns nil if verification fails.
-	Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User
 }
 
 // PasswordAuthenticator represents a source of authentication
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index b6b922de7a4d6..93806c7072aa2 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -20,6 +20,7 @@ import (
 // Ensure the struct implements the interface.
 var (
 	_ Method = &OAuth2{}
+	_ Named  = &OAuth2{}
 )
 
 // CheckOAuthAccessToken returns uid of user from oauth token
@@ -52,21 +53,11 @@ func CheckOAuthAccessToken(accessToken string) int64 {
 type OAuth2 struct {
 }
 
-// Init does nothing as the OAuth2 implementation does not need to allocate any resources
-func (o *OAuth2) Init() error {
-	return nil
-}
-
 // Name represents the name of auth method
 func (o *OAuth2) Name() string {
 	return "oauth2"
 }
 
-// Free does nothing as the OAuth2 implementation does not have to release any resources
-func (o *OAuth2) Free() error {
-	return nil
-}
-
 // userIDFromToken returns the user id corresponding to the OAuth token.
 func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
 	_ = req.ParseForm()
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index 90d830846e165..46d8d3fa6379d 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -20,6 +20,7 @@ import (
 // Ensure the struct implements the interface.
 var (
 	_ Method = &ReverseProxy{}
+	_ Named  = &ReverseProxy{}
 )
 
 // ReverseProxy implements the Auth interface, but actually relies on
@@ -44,16 +45,6 @@ func (r *ReverseProxy) Name() string {
 	return "reverse_proxy"
 }
 
-// Init does nothing as the ReverseProxy implementation does not need initialization
-func (r *ReverseProxy) Init() error {
-	return nil
-}
-
-// Free does nothing as the ReverseProxy implementation does not have to release resources
-func (r *ReverseProxy) Free() error {
-	return nil
-}
-
 // Verify extracts the username from the "setting.ReverseProxyAuthUser" header
 // of the request and returns the corresponding user object for that name.
 // Verification of header data is not performed as it should have already been done by
diff --git a/services/auth/session.go b/services/auth/session.go
index c3fcbc2bda67c..c93fa8c1939ba 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -21,21 +21,11 @@ var (
 type Session struct {
 }
 
-// Init does nothing as the Session implementation does not need to allocate any resources
-func (s *Session) Init() error {
-	return nil
-}
-
 // Name represents the name of auth method
 func (s *Session) Name() string {
 	return "session"
 }
 
-// Free does nothing as the Session implementation does not have to release any resources
-func (s *Session) Free() error {
-	return nil
-}
-
 // Verify checks if there is a user uid stored in the session and returns the user
 // object for that uid.
 // Returns nil if there is no user uid stored in the session.
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go
index 1d31ceaf9e1ce..c99dad712463f 100644
--- a/services/auth/sspi_windows.go
+++ b/services/auth/sspi_windows.go
@@ -33,7 +33,10 @@ var (
 	sspiAuth *websspi.Authenticator
 
 	// Ensure the struct implements the interface.
-	_ Method = &SSPI{}
+	_ Method        = &SSPI{}
+	_ Named         = &SSPI{}
+	_ Initializable = &SSPI{}
+	_ Freeable      = &SSPI{}
 )
 
 // SSPI implements the SingleSignOn interface and authenticates requests

From f26a8170a72e0cf4754711ffbd6622dcef7d5f11 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 05:02:09 +0100
Subject: [PATCH 27/44] fixup! Simplify auth.Method interface

---
 services/auth/session.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/services/auth/session.go b/services/auth/session.go
index c93fa8c1939ba..9a6e2d95d0d3c 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -14,6 +14,7 @@ import (
 // Ensure the struct implements the interface.
 var (
 	_ Method = &Session{}
+	_ Named  = &Session{}
 )
 
 // Session checks if there is a user uid stored in the session and returns the user

From 2db8e5d460939cec7422339fd3838652bdc718fd Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 05:05:02 +0100
Subject: [PATCH 28/44] fixup! fixup! Despecialize db authentication and fix
 bug

---
 models/login_source.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/models/login_source.go b/models/login_source.go
index da9189175b912..c27dcfa281bac 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -285,6 +285,9 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
 	source := new(LoginSource)
 	if id == 0 {
 		source.Cfg = registeredLoginConfigs[LoginNoType]()
+		// Set this source to active
+		// FIXME: allow disabling of db based password authentication in future
+		source.IsActived = true
 		return source, nil
 	}
 

From 46cae9df5959a66056134ba704bcbeb51e834015 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 05:10:40 +0100
Subject: [PATCH 29/44] fixup! Handle prohibit logins correctly

---
 services/auth/signin.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/auth/signin.go b/services/auth/signin.go
index 224688e8d1195..5b9fd81df3c7c 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -105,5 +105,5 @@ func UserSignIn(username, password string) (*models.User, error) {
 		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
 	}
 
-	return nil, models.ErrUserNotExist{UID: user.ID, Name: user.Name}
+	return nil, models.ErrUserNotExist{Name: username}
 }

From 65714ff0f008359196d3d4d05b9fa95ef51bfb29 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Mon, 21 Jun 2021 09:08:50 +0100
Subject: [PATCH 30/44] fixup! fixup! Simplify auth.Method interface

---
 services/auth/auth.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/services/auth/auth.go b/services/auth/auth.go
index 854abcacd1667..11a8c6ed1c999 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -57,12 +57,12 @@ func Init() {
 	}
 	specialInit()
 	for _, method := range Methods() {
-		initalizable, ok := method.(Initializable)
+		initializable, ok := method.(Initializable)
 		if !ok {
 			continue
 		}
 
-		err := initalizable.Init()
+		err := initializable.Init()
 		if err != nil {
 			log.Error("Could not initialize '%s' auth method, error: %s", reflect.TypeOf(method).String(), err)
 		}

From e1a015fca5e332dae203a6df25600c74ac7d139a Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Wed, 23 Jun 2021 22:32:11 +0100
Subject: [PATCH 31/44] Fix #16235

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/signin.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/services/auth/signin.go b/services/auth/signin.go
index 5b9fd81df3c7c..43135d60a97bd 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -102,7 +102,11 @@ func UserSignIn(username, password string) (*models.User, error) {
 			err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
 		}
 
-		log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+		if models.IsErrUserNotExist(err) {
+			log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err)
+		} else {
+			log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err)
+		}
 	}
 
 	return nil, models.ErrUserNotExist{Name: username}

From 339a74b67d3c6abce2327fda567d3feb6c1c35fb Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 27 Jun 2021 12:44:38 +0100
Subject: [PATCH 32/44] Fix #16252 - Equivalent to #16268

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/login_source.go                | 12 ++++++++++++
 models/repo_unit.go                   | 15 +++++----------
 services/auth/source/ldap/source.go   |  4 +---
 services/auth/source/oauth2/source.go |  3 +--
 services/auth/source/pam/source.go    |  3 +--
 services/auth/source/smtp/source.go   |  3 +--
 services/auth/source/sspi/source.go   |  3 +--
 7 files changed, 22 insertions(+), 21 deletions(-)

diff --git a/models/login_source.go b/models/login_source.go
index c27dcfa281bac..b048f6c5dc613 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -11,6 +11,7 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
+	jsoniter "github.com/json-iterator/go"
 
 	"xorm.io/xorm"
 	"xorm.io/xorm/convert"
@@ -126,6 +127,17 @@ func Cell2Int64(val xorm.Cell) int64 {
 	return (*val).(int64)
 }
 
+// JsonUnmarshalIgnoreErroneousBOM - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
+// possible that a Blob may gain an unwanted prefix of 0xff 0xfe.
+func JsonUnmarshalIgnoreErroneousBOM(bs []byte, v interface{}) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	err := json.Unmarshal(bs, &v)
+	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
+		err = json.Unmarshal(bs[2:], &v)
+	}
+	return err
+}
+
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	if colName == "type" {
diff --git a/models/repo_unit.go b/models/repo_unit.go
index 1d54579a6e727..5021b9a06f592 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -28,8 +28,7 @@ type UnitConfig struct{}
 
 // FromDB fills up a UnitConfig from serialized format.
 func (cfg *UnitConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
+	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a UnitConfig to a serialized format.
@@ -45,8 +44,7 @@ type ExternalWikiConfig struct {
 
 // FromDB fills up a ExternalWikiConfig from serialized format.
 func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
+	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a ExternalWikiConfig to a serialized format.
@@ -64,8 +62,7 @@ type ExternalTrackerConfig struct {
 
 // FromDB fills up a ExternalTrackerConfig from serialized format.
 func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
+	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a ExternalTrackerConfig to a serialized format.
@@ -83,8 +80,7 @@ type IssuesConfig struct {
 
 // FromDB fills up a IssuesConfig from serialized format.
 func (cfg *IssuesConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
+	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a IssuesConfig to a serialized format.
@@ -107,8 +103,7 @@ type PullRequestsConfig struct {
 
 // FromDB fills up a PullRequestsConfig from serialized format.
 func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &cfg)
+	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a PullRequestsConfig to a serialized format.
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index 80ed3e75e6e51..e3a685f7cd7b4 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -65,13 +65,11 @@ type wrappedSource struct {
 
 // FromDB fills up a LDAPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-
 	wrapped := &wrappedSource{
 		Source: source,
 	}
 
-	err := json.Unmarshal(bs, &wrapped)
+	err := models.JsonUnmarshalIgnoreErroneousBOM(bs, &wrapped)
 	if err != nil {
 		return err
 	}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 7780b5cc42638..4a59546731819 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -32,8 +32,7 @@ type Source struct {
 
 // FromDB fills up an OAuth2Config from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, source)
+	return models.JsonUnmarshalIgnoreErroneousBOM(bs, source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index 124d61974af97..431356f62cbb6 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -28,8 +28,7 @@ type Source struct {
 
 // FromDB fills up a PAMConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, &source)
+	return models.JsonUnmarshalIgnoreErroneousBOM(bs, &source)
 }
 
 // ToDB exports a PAMConfig to a serialized format.
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 104322dedc608..eb80110775d6f 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -32,8 +32,7 @@ type Source struct {
 
 // FromDB fills up an SMTPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, source)
+	return models.JsonUnmarshalIgnoreErroneousBOM(bs, source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
index fe0ff143c7698..2e09700ada0df 100644
--- a/services/auth/source/sspi/source.go
+++ b/services/auth/source/sspi/source.go
@@ -27,8 +27,7 @@ type Source struct {
 
 // FromDB fills up an SSPIConfig from serialized format.
 func (cfg *Source) FromDB(bs []byte) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Unmarshal(bs, cfg)
+	return models.JsonUnmarshalIgnoreErroneousBOM(bs, cfg)
 }
 
 // ToDB exports an SSPIConfig to a serialized format.

From 88d95469758bcb158c619a3c7a4ec70065387d89 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sun, 27 Jun 2021 15:40:18 +0100
Subject: [PATCH 33/44] fixup! Fix #16252 - Equivalent to #16268

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/helper.go                      | 15 +++++++++++++++
 models/login_source.go                | 12 ------------
 models/repo_unit.go                   | 10 +++++-----
 services/auth/source/ldap/source.go   |  2 +-
 services/auth/source/oauth2/source.go |  2 +-
 services/auth/source/pam/source.go    |  2 +-
 services/auth/source/smtp/source.go   |  2 +-
 services/auth/source/sspi/source.go   |  2 +-
 8 files changed, 25 insertions(+), 22 deletions(-)

diff --git a/models/helper.go b/models/helper.go
index 91063b2d131ae..b839d06bf5762 100644
--- a/models/helper.go
+++ b/models/helper.go
@@ -4,6 +4,10 @@
 
 package models
 
+import (
+	jsoniter "github.com/json-iterator/go"
+)
+
 func keysInt64(m map[int64]struct{}) []int64 {
 	keys := make([]int64, 0, len(m))
 	for k := range m {
@@ -27,3 +31,14 @@ func valuesUser(m map[int64]*User) []*User {
 	}
 	return values
 }
+
+// JSONUnmarshalIgnoreErroneousBOM - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
+// possible that a Blob may gain an unwanted prefix of 0xff 0xfe.
+func JSONUnmarshalIgnoreErroneousBOM(bs []byte, v interface{}) error {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	err := json.Unmarshal(bs, &v)
+	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
+		err = json.Unmarshal(bs[2:], &v)
+	}
+	return err
+}
diff --git a/models/login_source.go b/models/login_source.go
index b048f6c5dc613..c27dcfa281bac 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -11,7 +11,6 @@ import (
 
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
-	jsoniter "github.com/json-iterator/go"
 
 	"xorm.io/xorm"
 	"xorm.io/xorm/convert"
@@ -127,17 +126,6 @@ func Cell2Int64(val xorm.Cell) int64 {
 	return (*val).(int64)
 }
 
-// JsonUnmarshalIgnoreErroneousBOM - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
-// possible that a Blob may gain an unwanted prefix of 0xff 0xfe.
-func JsonUnmarshalIgnoreErroneousBOM(bs []byte, v interface{}) error {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	err := json.Unmarshal(bs, &v)
-	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
-		err = json.Unmarshal(bs[2:], &v)
-	}
-	return err
-}
-
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) {
 	if colName == "type" {
diff --git a/models/repo_unit.go b/models/repo_unit.go
index 5021b9a06f592..6edb7e1853ac4 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -28,7 +28,7 @@ type UnitConfig struct{}
 
 // FromDB fills up a UnitConfig from serialized format.
 func (cfg *UnitConfig) FromDB(bs []byte) error {
-	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return JSONUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a UnitConfig to a serialized format.
@@ -44,7 +44,7 @@ type ExternalWikiConfig struct {
 
 // FromDB fills up a ExternalWikiConfig from serialized format.
 func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
-	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return JSONUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a ExternalWikiConfig to a serialized format.
@@ -62,7 +62,7 @@ type ExternalTrackerConfig struct {
 
 // FromDB fills up a ExternalTrackerConfig from serialized format.
 func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
-	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return JSONUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a ExternalTrackerConfig to a serialized format.
@@ -80,7 +80,7 @@ type IssuesConfig struct {
 
 // FromDB fills up a IssuesConfig from serialized format.
 func (cfg *IssuesConfig) FromDB(bs []byte) error {
-	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return JSONUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a IssuesConfig to a serialized format.
@@ -103,7 +103,7 @@ type PullRequestsConfig struct {
 
 // FromDB fills up a PullRequestsConfig from serialized format.
 func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
-	return JsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return JSONUnmarshalIgnoreErroneousBOM(bs, &cfg)
 }
 
 // ToDB exports a PullRequestsConfig to a serialized format.
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index e3a685f7cd7b4..affbcfbd99b1e 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -69,7 +69,7 @@ func (source *Source) FromDB(bs []byte) error {
 		Source: source,
 	}
 
-	err := models.JsonUnmarshalIgnoreErroneousBOM(bs, &wrapped)
+	err := models.JSONUnmarshalIgnoreErroneousBOM(bs, &wrapped)
 	if err != nil {
 		return err
 	}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 4a59546731819..f054e7960ed7b 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -32,7 +32,7 @@ type Source struct {
 
 // FromDB fills up an OAuth2Config from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	return models.JsonUnmarshalIgnoreErroneousBOM(bs, source)
+	return models.JSONUnmarshalIgnoreErroneousBOM(bs, source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index 431356f62cbb6..4655f32d868cf 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -28,7 +28,7 @@ type Source struct {
 
 // FromDB fills up a PAMConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	return models.JsonUnmarshalIgnoreErroneousBOM(bs, &source)
+	return models.JSONUnmarshalIgnoreErroneousBOM(bs, &source)
 }
 
 // ToDB exports a PAMConfig to a serialized format.
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index eb80110775d6f..3a9a816a0e268 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -32,7 +32,7 @@ type Source struct {
 
 // FromDB fills up an SMTPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	return models.JsonUnmarshalIgnoreErroneousBOM(bs, source)
+	return models.JSONUnmarshalIgnoreErroneousBOM(bs, source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
index 2e09700ada0df..949d2bab8ea6b 100644
--- a/services/auth/source/sspi/source.go
+++ b/services/auth/source/sspi/source.go
@@ -27,7 +27,7 @@ type Source struct {
 
 // FromDB fills up an SSPIConfig from serialized format.
 func (cfg *Source) FromDB(bs []byte) error {
-	return models.JsonUnmarshalIgnoreErroneousBOM(bs, cfg)
+	return models.JSONUnmarshalIgnoreErroneousBOM(bs, cfg)
 }
 
 // ToDB exports an SSPIConfig to a serialized format.

From ee5f3985cefe8466208f75ef415c4a619b7377cc Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Wed, 14 Jul 2021 22:22:41 +0100
Subject: [PATCH 34/44] as per lafriks use migration instead

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 .../Test_unwrapLDAPSourceCfg/login_source.yml | 41 ++++++++++
 models/migrations/migrations.go               |  2 +
 models/migrations/v189.go                     | 79 +++++++++++++++++++
 models/migrations/v189_test.go                | 49 ++++++++++++
 services/auth/source/ldap/source.go           | 16 +---
 5 files changed, 173 insertions(+), 14 deletions(-)
 create mode 100644 models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
 create mode 100644 models/migrations/v189.go
 create mode 100644 models/migrations/v189_test.go

diff --git a/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
new file mode 100644
index 0000000000000..a9e699fcc2e32
--- /dev/null
+++ b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
@@ -0,0 +1,41 @@
+# type LoginSource struct {
+#   ID        int64 `xorm:"pk autoincr"`
+#   Type      int
+#   Cfg       []byte `xorm:"TEXT"`
+#   Expected  []byte `xorm:"TEXT"`
+# }
+-
+  id: 1
+  type: 1
+  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+-
+  id: 2
+  type: 2
+  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  expected: "{\"A\":\"string\",\"B\":1}"
+-
+  id: 3
+  type: 3
+  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+-
+  id: 4
+  type: 4
+  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+-
+  id: 5
+  type: 5
+  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  expected: "{\"A\":\"string\",\"B\":1}"
+-
+  id: 6
+  type: 2
+  cfg: "{\"A\":\"string\",\"B\":1}"
+  expected: "{\"A\":\"string\",\"B\":1}"
+-
+  id: 7
+  type: 5
+  cfg: "{\"A\":\"string\",\"B\":1}"
+  expected: "{\"A\":\"string\",\"B\":1}"
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 7a4193199c2f4..fed7b909c1ae2 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -327,6 +327,8 @@ var migrations = []Migration{
 	NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
 	// v188 -> v189
 	NewMigration("Add key is verified to gpg key", addKeyIsVerified),
+	// v189 -> v190
+	NewMigration("Unwrap ldap.Sources", unwrapLDAPSourceCfg),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v189.go b/models/migrations/v189.go
new file mode 100644
index 0000000000000..06c558a7c8edc
--- /dev/null
+++ b/models/migrations/v189.go
@@ -0,0 +1,79 @@
+// Copyright 2021 The Gitea 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 migrations
+
+import (
+	"fmt"
+
+	jsoniter "github.com/json-iterator/go"
+	"xorm.io/xorm"
+)
+
+func unwrapLDAPSourceCfg(x *xorm.Engine) error {
+	jsonUnmarshalIgnoreErroneousBOM := func(bs []byte, v interface{}) error {
+		json := jsoniter.ConfigCompatibleWithStandardLibrary
+		err := json.Unmarshal(bs, &v)
+		if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
+			err = json.Unmarshal(bs[2:], &v)
+		}
+		return err
+	}
+
+	// LoginSource represents an external way for authorizing users.
+	type LoginSource struct {
+		ID        int64 `xorm:"pk autoincr"`
+		Type      int
+		IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
+		Cfg       string `xorm:"TEXT"`
+	}
+
+	const ldapType = 2
+	const dldapType = 5
+
+	type WrappedSource struct {
+		Source map[string]interface{}
+	}
+
+	// change lower_email as unique
+	if err := x.Sync2(new(LoginSource)); err != nil {
+		return err
+	}
+
+	sess := x.NewSession()
+	defer sess.Close()
+
+	const batchSize = 100
+	for start := 0; ; start += batchSize {
+		sources := make([]*LoginSource, 0, batchSize)
+		if err := sess.Limit(batchSize, start).Where("`type` = ? OR `type` = ?", ldapType, dldapType).Find(&sources); err != nil {
+			return err
+		}
+		if len(sources) == 0 {
+			break
+		}
+
+		for _, source := range sources {
+			wrapped := &WrappedSource{
+				Source: map[string]interface{}{},
+			}
+			err := jsonUnmarshalIgnoreErroneousBOM([]byte(source.Cfg), wrapped)
+			if err != nil {
+				return fmt.Errorf("failed to unmarshal %s: %w", string(source.Cfg), err)
+			}
+			if wrapped.Source != nil && len(wrapped.Source) > 0 {
+				bs, err := jsoniter.Marshal(wrapped.Source)
+				if err != nil {
+					return err
+				}
+				source.Cfg = string(bs)
+				if _, err := sess.ID(source.ID).Cols("cfg").Update(source); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/models/migrations/v189_test.go b/models/migrations/v189_test.go
new file mode 100644
index 0000000000000..d0be065f7c5bc
--- /dev/null
+++ b/models/migrations/v189_test.go
@@ -0,0 +1,49 @@
+// Copyright 2021 The Gitea 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 migrations
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_unwrapLDAPSourceCfg(t *testing.T) {
+	// LoginSource represents an external way for authorizing users.
+	type LoginSource struct {
+		ID        int64 `xorm:"pk autoincr"`
+		Type      int
+		IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
+		Cfg       string `xorm:"TEXT"`
+		Expected  string `xorm:"TEXT"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := prepareTestEnv(t, 0, new(LoginSource))
+	if x == nil || t.Failed() {
+		defer deferable()
+		return
+	}
+	defer deferable()
+
+	// Run the migration
+	if err := unwrapLDAPSourceCfg(x); err != nil {
+		assert.NoError(t, err)
+		return
+	}
+
+	const batchSize = 100
+	for start := 0; ; start += batchSize {
+		sources := make([]*LoginSource, 0, batchSize)
+		if len(sources) == 0 {
+			break
+		}
+
+		for _, source := range sources {
+			assert.Equal(t, string(source.Cfg), string(source.Expected), "unwrapLDAPSourceCfg failed for %d", source.ID)
+		}
+	}
+
+}
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index affbcfbd99b1e..8fc815eb2c9d6 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -58,18 +58,9 @@ type Source struct {
 	loginSource *models.LoginSource
 }
 
-// wrappedSource wraps the source to ensure that the FromDB/ToDB results are the same as previously
-type wrappedSource struct {
-	Source *Source
-}
-
 // FromDB fills up a LDAPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	wrapped := &wrappedSource{
-		Source: source,
-	}
-
-	err := models.JSONUnmarshalIgnoreErroneousBOM(bs, &wrapped)
+	err := models.JSONUnmarshalIgnoreErroneousBOM(bs, source)
 	if err != nil {
 		return err
 	}
@@ -89,10 +80,7 @@ func (source *Source) ToDB() ([]byte, error) {
 	}
 	source.BindPassword = ""
 	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	wrapped := &wrappedSource{
-		Source: source,
-	}
-	return json.Marshal(wrapped)
+	return json.Marshal(source)
 }
 
 // SecurityProtocolName returns the name of configured security

From 3497354ddbd39280addb8c36e5578648f884567c Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 12:29:20 +0100
Subject: [PATCH 35/44] fix tests

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 .../Test_unwrapLDAPSourceCfg/login_source.yml | 24 +++++++++----------
 models/migrations/v189_test.go                |  5 ++++
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
index a9e699fcc2e32..f753d13110cac 100644
--- a/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
+++ b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
@@ -12,30 +12,30 @@
 -
   id: 2
   type: 2
-  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
-  expected: "{\"A\":\"string\",\"B\":1}"
+  cfg: "{\"Source\":{\"A\":\"string2\",\"B\":2}}"
+  expected: "{\"A\":\"string2\",\"B\":2}"
 -
   id: 3
   type: 3
-  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
-  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  cfg: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
+  expected: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
 -
   id: 4
   type: 4
-  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
-  expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
+  cfg: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
+  expected: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
 -
   id: 5
   type: 5
-  cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
-  expected: "{\"A\":\"string\",\"B\":1}"
+  cfg: "{\"Source\":{\"A\":\"string5\",\"B\":5}}"
+  expected: "{\"A\":\"string5\",\"B\":5}"
 -
   id: 6
   type: 2
-  cfg: "{\"A\":\"string\",\"B\":1}"
-  expected: "{\"A\":\"string\",\"B\":1}"
+  cfg: "{\"A\":\"string6\",\"B\":6}"
+  expected: "{\"A\":\"string6\",\"B\":6}"
 -
   id: 7
   type: 5
-  cfg: "{\"A\":\"string\",\"B\":1}"
-  expected: "{\"A\":\"string\",\"B\":1}"
+  cfg: "{\"A\":\"string7\",\"B\":7}"
+  expected: "{\"A\":\"string7\",\"B\":7}"
diff --git a/models/migrations/v189_test.go b/models/migrations/v189_test.go
index d0be065f7c5bc..b891b7eac4573 100644
--- a/models/migrations/v189_test.go
+++ b/models/migrations/v189_test.go
@@ -37,6 +37,11 @@ func Test_unwrapLDAPSourceCfg(t *testing.T) {
 	const batchSize = 100
 	for start := 0; ; start += batchSize {
 		sources := make([]*LoginSource, 0, batchSize)
+		if err := x.Table("login_source").Limit(batchSize, start).Find(&sources); err != nil {
+			assert.NoError(t, err)
+			return
+		}
+
 		if len(sources) == 0 {
 			break
 		}

From f1fb660fa5902d2f99f63da232e35083385db3bd Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 13:16:33 +0100
Subject: [PATCH 36/44] fix test

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/migrations/v189_test.go | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/models/migrations/v189_test.go b/models/migrations/v189_test.go
index b891b7eac4573..6e2b4df69bf33 100644
--- a/models/migrations/v189_test.go
+++ b/models/migrations/v189_test.go
@@ -7,6 +7,7 @@ package migrations
 import (
 	"testing"
 
+	jsoniter "github.com/json-iterator/go"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -47,7 +48,20 @@ func Test_unwrapLDAPSourceCfg(t *testing.T) {
 		}
 
 		for _, source := range sources {
-			assert.Equal(t, string(source.Cfg), string(source.Expected), "unwrapLDAPSourceCfg failed for %d", source.ID)
+			converted := map[string]interface{}{}
+			expected := map[string]interface{}{}
+
+			if err := jsoniter.Unmarshal([]byte(source.Cfg), &converted); err != nil {
+				assert.NoError(t, err)
+				return
+			}
+
+			if err := jsoniter.Unmarshal([]byte(source.Expected), &expected); err != nil {
+				assert.NoError(t, err)
+				return
+			}
+
+			assert.EqualValues(t, expected, converted, "unwrapLDAPSourceCfg failed for %d", source.ID)
 		}
 	}
 

From 28b57480223f0532f417ec9908b77dd6de3ce00b Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 18:28:03 +0100
Subject: [PATCH 37/44] not sure if this will help

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/migrations/v189.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/models/migrations/v189.go b/models/migrations/v189.go
index 06c558a7c8edc..b3b805a48b446 100644
--- a/models/migrations/v189.go
+++ b/models/migrations/v189.go
@@ -58,7 +58,7 @@ func unwrapLDAPSourceCfg(x *xorm.Engine) error {
 			wrapped := &WrappedSource{
 				Source: map[string]interface{}{},
 			}
-			err := jsonUnmarshalIgnoreErroneousBOM([]byte(source.Cfg), wrapped)
+			err := jsonUnmarshalIgnoreErroneousBOM([]byte(source.Cfg), &wrapped)
 			if err != nil {
 				return fmt.Errorf("failed to unmarshal %s: %w", string(source.Cfg), err)
 			}

From aa478358ad2b3532e8e580820e6cae9b6cba5daa Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 18:50:30 +0100
Subject: [PATCH 38/44] And rename IsActived to IsActive

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/admin.go                                  | 10 ++--
 cmd/admin_auth_ldap.go                        | 10 ++--
 cmd/admin_auth_ldap_test.go                   | 56 +++++++++----------
 models/login_source.go                        | 12 ++--
 .../Test_unwrapLDAPSourceCfg/login_source.yml |  7 +++
 models/migrations/v189.go                     | 14 ++++-
 models/migrations/v189_test.go                | 33 ++++++++---
 models/oauth2.go                              |  4 +-
 routers/web/admin/auths.go                    |  4 +-
 services/auth/signin.go                       |  4 +-
 services/auth/source/oauth2/init.go           |  2 +-
 services/auth/sync.go                         |  2 +-
 templates/admin/auth/edit.tmpl                |  2 +-
 templates/admin/auth/list.tmpl                |  2 +-
 .../user/settings/security_accountlinks.tmpl  |  2 +-
 15 files changed, 99 insertions(+), 65 deletions(-)

diff --git a/cmd/admin.go b/cmd/admin.go
index 5a0da5cb8d97e..69c8a5669af5d 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -626,10 +626,10 @@ func runAddOauth(c *cli.Context) error {
 	}
 
 	return models.CreateLoginSource(&models.LoginSource{
-		Type:      models.LoginOAuth2,
-		Name:      c.String("name"),
-		IsActived: true,
-		Cfg:       parseOAuth2Config(c),
+		Type:     models.LoginOAuth2,
+		Name:     c.String("name"),
+		IsActive: true,
+		Cfg:      parseOAuth2Config(c),
 	})
 }
 
@@ -729,7 +729,7 @@ func runListAuth(c *cli.Context) error {
 	w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
 	fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
 	for _, source := range loginSources {
-		fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActived)
+		fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActive)
 	}
 	w.Flush()
 
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index 6427add8ab42b..4314930a3e083 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -172,7 +172,7 @@ func parseLoginSource(c *cli.Context, loginSource *models.LoginSource) {
 		loginSource.Name = c.String("name")
 	}
 	if c.IsSet("not-active") {
-		loginSource.IsActived = !c.Bool("not-active")
+		loginSource.IsActive = !c.Bool("not-active")
 	}
 	if c.IsSet("synchronize-users") {
 		loginSource.IsSyncEnabled = c.Bool("synchronize-users")
@@ -289,8 +289,8 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 	}
 
 	loginSource := &models.LoginSource{
-		Type:      models.LoginLDAP,
-		IsActived: true, // active by default
+		Type:     models.LoginLDAP,
+		IsActive: true, // active by default
 		Cfg: &ldap.Source{
 			Enabled: true, // always true
 		},
@@ -334,8 +334,8 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 	}
 
 	loginSource := &models.LoginSource{
-		Type:      models.LoginDLDAP,
-		IsActived: true, // active by default
+		Type:     models.LoginDLDAP,
+		IsActive: true, // active by default
 		Cfg: &ldap.Source{
 			Enabled: true, // always true
 		},
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index bcf4325f0601b..692b11e3f422d 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -54,7 +54,7 @@ func TestAddLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				Name:          "ldap (via Bind DN) source full",
-				IsActived:     false,
+				IsActive:      false,
 				IsSyncEnabled: true,
 				Cfg: &ldap.Source{
 					Name:                  "ldap (via Bind DN) source full",
@@ -92,9 +92,9 @@ func TestAddLdapBindDn(t *testing.T) {
 				"--email-attribute", "mail-bind min",
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				Name:      "ldap (via Bind DN) source min",
-				IsActived: true,
+				Type:     models.LoginLDAP,
+				Name:     "ldap (via Bind DN) source min",
+				IsActive: true,
 				Cfg: &ldap.Source{
 					Name:             "ldap (via Bind DN) source min",
 					Host:             "ldap-bind-server min",
@@ -272,9 +272,9 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				"--user-dn", "cn=%s,ou=Users,dc=full-domain-simple,dc=org",
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				Name:      "ldap (simple auth) source full",
-				IsActived: false,
+				Type:     models.LoginDLDAP,
+				Name:     "ldap (simple auth) source full",
+				IsActive: false,
 				Cfg: &ldap.Source{
 					Name:                  "ldap (simple auth) source full",
 					Host:                  "ldap-simple-server full",
@@ -308,9 +308,9 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 				"--user-dn", "cn=%s,ou=Users,dc=min-domain-simple,dc=org",
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				Name:      "ldap (simple auth) source min",
-				IsActived: true,
+				Type:     models.LoginDLDAP,
+				Name:     "ldap (simple auth) source min",
+				IsActive: true,
 				Cfg: &ldap.Source{
 					Name:             "ldap (simple auth) source min",
 					Host:             "ldap-simple-server min",
@@ -508,8 +508,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			id: 23,
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: true,
+				Type:     models.LoginLDAP,
+				IsActive: true,
 				Cfg: &ldap.Source{
 					Enabled: true,
 				},
@@ -517,7 +517,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				Name:          "ldap (via Bind DN) source full",
-				IsActived:     false,
+				IsActive:      false,
 				IsSyncEnabled: true,
 				Cfg: &ldap.Source{
 					Name:                  "ldap (via Bind DN) source full",
@@ -576,14 +576,14 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				"--not-active",
 			},
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: true,
-				Cfg:       &ldap.Source{},
+				Type:     models.LoginLDAP,
+				IsActive: true,
+				Cfg:      &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: false,
-				Cfg:       &ldap.Source{},
+				Type:     models.LoginLDAP,
+				IsActive: false,
+				Cfg:      &ldap.Source{},
 			},
 		},
 		// case 4
@@ -936,9 +936,9 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			id: 7,
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				Name:      "ldap (simple auth) source full",
-				IsActived: false,
+				Type:     models.LoginDLDAP,
+				Name:     "ldap (simple auth) source full",
+				IsActive: false,
 				Cfg: &ldap.Source{
 					Name:                  "ldap (simple auth) source full",
 					Host:                  "ldap-simple-server full",
@@ -992,14 +992,14 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				"--not-active",
 			},
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				IsActived: true,
-				Cfg:       &ldap.Source{},
+				Type:     models.LoginDLDAP,
+				IsActive: true,
+				Cfg:      &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				IsActived: false,
-				Cfg:       &ldap.Source{},
+				Type:     models.LoginDLDAP,
+				IsActive: false,
+				Cfg:      &ldap.Source{},
 			},
 		},
 		// case 4
diff --git a/models/login_source.go b/models/login_source.go
index ff35ee377c518..5e1c6e222435e 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -105,7 +105,7 @@ type LoginSource struct {
 	ID            int64 `xorm:"pk autoincr"`
 	Type          LoginType
 	Name          string             `xorm:"UNIQUE"`
-	IsActived     bool               `xorm:"INDEX NOT NULL DEFAULT false"`
+	IsActive      bool               `xorm:"INDEX NOT NULL DEFAULT false"`
 	IsSyncEnabled bool               `xorm:"INDEX NOT NULL DEFAULT false"`
 	Cfg           convert.Conversion `xorm:"TEXT"`
 
@@ -214,7 +214,7 @@ func CreateLoginSource(source *LoginSource) error {
 		return err
 	}
 
-	if !source.IsActived {
+	if !source.IsActive {
 		return nil
 	}
 
@@ -251,7 +251,7 @@ func LoginSourcesByType(loginType LoginType) ([]*LoginSource, error) {
 // AllActiveLoginSources returns all active sources
 func AllActiveLoginSources() ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 5)
-	if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
+	if err := x.Where("is_active = ?", true).Find(&sources); err != nil {
 		return nil, err
 	}
 	return sources, nil
@@ -260,7 +260,7 @@ func AllActiveLoginSources() ([]*LoginSource, error) {
 // ActiveLoginSources returns all active sources of the specified type
 func ActiveLoginSources(loginType LoginType) ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 1)
-	if err := x.Where("is_actived = ? and type = ?", true, loginType).Find(&sources); err != nil {
+	if err := x.Where("is_active = ? and type = ?", true, loginType).Find(&sources); err != nil {
 		return nil, err
 	}
 	return sources, nil
@@ -287,7 +287,7 @@ func GetLoginSourceByID(id int64) (*LoginSource, error) {
 		source.Cfg = registeredLoginConfigs[LoginNoType]()
 		// Set this source to active
 		// FIXME: allow disabling of db based password authentication in future
-		source.IsActived = true
+		source.IsActive = true
 		return source, nil
 	}
 
@@ -316,7 +316,7 @@ func UpdateSource(source *LoginSource) error {
 		return err
 	}
 
-	if !source.IsActived {
+	if !source.IsActive {
 		return nil
 	}
 
diff --git a/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
index f753d13110cac..4b72ba145ecb8 100644
--- a/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
+++ b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
@@ -7,35 +7,42 @@
 -
   id: 1
   type: 1
+  is_actived: false
   cfg: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
   expected: "{\"Source\":{\"A\":\"string\",\"B\":1}}"
 -
   id: 2
   type: 2
+  is_actived: true
   cfg: "{\"Source\":{\"A\":\"string2\",\"B\":2}}"
   expected: "{\"A\":\"string2\",\"B\":2}"
 -
   id: 3
   type: 3
+  is_actived: false
   cfg: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
   expected: "{\"Source\":{\"A\":\"string3\",\"B\":3}}"
 -
   id: 4
   type: 4
+  is_actived: true
   cfg: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
   expected: "{\"Source\":{\"A\":\"string4\",\"B\":4}}"
 -
   id: 5
   type: 5
+  is_actived: false
   cfg: "{\"Source\":{\"A\":\"string5\",\"B\":5}}"
   expected: "{\"A\":\"string5\",\"B\":5}"
 -
   id: 6
   type: 2
+  is_actived: true
   cfg: "{\"A\":\"string6\",\"B\":6}"
   expected: "{\"A\":\"string6\",\"B\":6}"
 -
   id: 7
   type: 5
+  is_actived: false
   cfg: "{\"A\":\"string7\",\"B\":7}"
   expected: "{\"A\":\"string7\",\"B\":7}"
diff --git a/models/migrations/v189.go b/models/migrations/v189.go
index b3b805a48b446..08aec7de0458e 100644
--- a/models/migrations/v189.go
+++ b/models/migrations/v189.go
@@ -26,6 +26,7 @@ func unwrapLDAPSourceCfg(x *xorm.Engine) error {
 		ID        int64 `xorm:"pk autoincr"`
 		Type      int
 		IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
+		IsActive  bool   `xorm:"INDEX NOT NULL DEFAULT false"`
 		Cfg       string `xorm:"TEXT"`
 	}
 
@@ -75,5 +76,16 @@ func unwrapLDAPSourceCfg(x *xorm.Engine) error {
 		}
 	}
 
-	return nil
+	if _, err := x.SetExpr("is_active", "is_actived").Update(&LoginSource{}); err != nil {
+		return fmt.Errorf("SetExpr Update failed:  %w", err)
+	}
+
+	if err := sess.Begin(); err != nil {
+		return err
+	}
+	if err := dropTableColumns(sess, "login_source", "is_actived"); err != nil {
+		return err
+	}
+
+	return sess.Commit()
 }
diff --git a/models/migrations/v189_test.go b/models/migrations/v189_test.go
index 6e2b4df69bf33..7a740948bb837 100644
--- a/models/migrations/v189_test.go
+++ b/models/migrations/v189_test.go
@@ -11,24 +11,38 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+// LoginSource represents an external way for authorizing users.
+type LoginSourceOriginal_v189 struct {
+	ID        int64 `xorm:"pk autoincr"`
+	Type      int
+	IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
+	Cfg       string `xorm:"TEXT"`
+	Expected  string `xorm:"TEXT"`
+}
+
+func (ls *LoginSourceOriginal_v189) TableName() string {
+	return "login_source"
+}
+
 func Test_unwrapLDAPSourceCfg(t *testing.T) {
-	// LoginSource represents an external way for authorizing users.
-	type LoginSource struct {
-		ID        int64 `xorm:"pk autoincr"`
-		Type      int
-		IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
-		Cfg       string `xorm:"TEXT"`
-		Expected  string `xorm:"TEXT"`
-	}
 
 	// Prepare and load the testing database
-	x, deferable := prepareTestEnv(t, 0, new(LoginSource))
+	x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginal_v189))
 	if x == nil || t.Failed() {
 		defer deferable()
 		return
 	}
 	defer deferable()
 
+	// LoginSource represents an external way for authorizing users.
+	type LoginSource struct {
+		ID       int64 `xorm:"pk autoincr"`
+		Type     int
+		IsActive bool   `xorm:"INDEX NOT NULL DEFAULT false"`
+		Cfg      string `xorm:"TEXT"`
+		Expected string `xorm:"TEXT"`
+	}
+
 	// Run the migration
 	if err := unwrapLDAPSourceCfg(x); err != nil {
 		assert.NoError(t, err)
@@ -62,6 +76,7 @@ func Test_unwrapLDAPSourceCfg(t *testing.T) {
 			}
 
 			assert.EqualValues(t, expected, converted, "unwrapLDAPSourceCfg failed for %d", source.ID)
+			assert.EqualValues(t, source.ID%2 == 0, source.IsActive, "unwrapLDAPSourceCfg failed for %d", source.ID)
 		}
 	}
 
diff --git a/models/oauth2.go b/models/oauth2.go
index ad5761e525aa5..127e8d760321b 100644
--- a/models/oauth2.go
+++ b/models/oauth2.go
@@ -7,7 +7,7 @@ package models
 // GetActiveOAuth2ProviderLoginSources returns all actived LoginOAuth2 sources
 func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 	sources := make([]*LoginSource, 0, 1)
-	if err := x.Where("is_actived = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
+	if err := x.Where("is_active = ? and type = ?", true, LoginOAuth2).Find(&sources); err != nil {
 		return nil, err
 	}
 	return sources, nil
@@ -16,7 +16,7 @@ func GetActiveOAuth2ProviderLoginSources() ([]*LoginSource, error) {
 // GetActiveOAuth2LoginSourceByName returns a OAuth2 LoginSource based on the given name
 func GetActiveOAuth2LoginSourceByName(name string) (*LoginSource, error) {
 	loginSource := new(LoginSource)
-	has, err := x.Where("name = ? and type = ? and is_actived = ?", name, LoginOAuth2, true).Get(loginSource)
+	has, err := x.Where("name = ? and type = ? and is_active = ?", name, LoginOAuth2, true).Get(loginSource)
 	if !has || err != nil {
 		return nil, err
 	}
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 19c24ce251c3b..20efd4a2aca03 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -272,7 +272,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 	if err := models.CreateLoginSource(&models.LoginSource{
 		Type:          models.LoginType(form.Type),
 		Name:          form.Name,
-		IsActived:     form.IsActive,
+		IsActive:      form.IsActive,
 		IsSyncEnabled: form.IsSyncEnabled,
 		Cfg:           config,
 	}); err != nil {
@@ -365,7 +365,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 	}
 
 	source.Name = form.Name
-	source.IsActived = form.IsActive
+	source.IsActive = form.IsActive
 	source.IsSyncEnabled = form.IsSyncEnabled
 	source.Cfg = config
 	if err := models.UpdateSource(source); err != nil {
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 43135d60a97bd..2c4bf9b35b7e3 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -54,7 +54,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 			return nil, err
 		}
 
-		if !source.IsActived {
+		if !source.IsActive {
 			return nil, models.ErrLoginSourceNotActived
 		}
 
@@ -83,7 +83,7 @@ func UserSignIn(username, password string) (*models.User, error) {
 	}
 
 	for _, source := range sources {
-		if !source.IsActived {
+		if !source.IsActive {
 			// don't try to authenticate non-active sources
 			continue
 		}
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index f602e6725afb3..f797fd7fd4852 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -72,7 +72,7 @@ func initOAuth2LoginSources() error {
 		err := oauth2Source.RegisterSource()
 		if err != nil {
 			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
-			source.IsActived = false
+			source.IsActive = false
 			if err = models.UpdateSource(source); err != nil {
 				log.Critical("Unable to update source %s to disable it. Error: %v", err)
 				return err
diff --git a/services/auth/sync.go b/services/auth/sync.go
index a608787aad891..a34b4d1d2694f 100644
--- a/services/auth/sync.go
+++ b/services/auth/sync.go
@@ -22,7 +22,7 @@ func SyncExternalUsers(ctx context.Context, updateExisting bool) error {
 	}
 
 	for _, s := range ls {
-		if !s.IsActived || !s.IsSyncEnabled {
+		if !s.IsActive || !s.IsSyncEnabled {
 			continue
 		}
 		select {
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index d825cd7d12de6..01c4b57395c9d 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -325,7 +325,7 @@
 				<div class="inline field">
 					<div class="ui checkbox">
 						<label><strong>{{.i18n.Tr "admin.auths.activated"}}</strong></label>
-						<input name="is_active" type="checkbox" {{if .Source.IsActived}}checked{{end}}>
+						<input name="is_active" type="checkbox" {{if .Source.IsActive}}checked{{end}}>
 					</div>
 				</div>
 
diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl
index d5d8aadb56315..35ab976022bb2 100644
--- a/templates/admin/auth/list.tmpl
+++ b/templates/admin/auth/list.tmpl
@@ -28,7 +28,7 @@
 							<td>{{.ID}}</td>
 							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td>
 							<td>{{.TypeName}}</td>
-							<td>{{if .IsActived}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
+							<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
 							<td><span class="poping up" data-content="{{.UpdatedUnix.FormatShort}}" data-variation="tiny">{{.UpdatedUnix.FormatShort}}</span></td>
 							<td><span class="poping up" data-content="{{.CreatedUnix.FormatLong}}" data-variation="tiny">{{.CreatedUnix.FormatShort}}</span></td>
 							<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
diff --git a/templates/user/settings/security_accountlinks.tmpl b/templates/user/settings/security_accountlinks.tmpl
index 9c2436dd3f76b..5aa9282083b61 100644
--- a/templates/user/settings/security_accountlinks.tmpl
+++ b/templates/user/settings/security_accountlinks.tmpl
@@ -16,7 +16,7 @@
 				</div>
 					<div class="content">
 						<strong>{{$provider}}</strong>
-						{{if $loginSource.IsActived}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}}
+						{{if $loginSource.IsActive}}<span class="text red">{{$.i18n.Tr "settings.active"}}</span>{{end}}
 					</div>
 			</div>
 		{{end}}

From 5244f402c0ad85a65b75a60188fa039684d73218 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 18:52:58 +0100
Subject: [PATCH 39/44] fixup! And rename IsActived to IsActive

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/migrations/v189_test.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/models/migrations/v189_test.go b/models/migrations/v189_test.go
index 7a740948bb837..f4fe6dec3f9ee 100644
--- a/models/migrations/v189_test.go
+++ b/models/migrations/v189_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 // LoginSource represents an external way for authorizing users.
-type LoginSourceOriginal_v189 struct {
+type LoginSourceOriginalV189 struct {
 	ID        int64 `xorm:"pk autoincr"`
 	Type      int
 	IsActived bool   `xorm:"INDEX NOT NULL DEFAULT false"`
@@ -20,14 +20,14 @@ type LoginSourceOriginal_v189 struct {
 	Expected  string `xorm:"TEXT"`
 }
 
-func (ls *LoginSourceOriginal_v189) TableName() string {
+func (ls *LoginSourceOriginalV189) TableName() string {
 	return "login_source"
 }
 
 func Test_unwrapLDAPSourceCfg(t *testing.T) {
 
 	// Prepare and load the testing database
-	x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginal_v189))
+	x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginalV189))
 	if x == nil || t.Failed() {
 		defer deferable()
 		return

From 28c8f311e93c3dd14a6665fa40e49c424c1573ff Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Thu, 15 Jul 2021 19:28:18 +0100
Subject: [PATCH 40/44] Delete db at the end of migration tests

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 models/migrations/migrations_test.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go
index 26066580d8978..634bfc84865dc 100644
--- a/models/migrations/migrations_test.go
+++ b/models/migrations/migrations_test.go
@@ -220,6 +220,9 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
 			if err := x.Close(); err != nil {
 				t.Errorf("error during close: %v", err)
 			}
+			if err := deleteDB(); err != nil {
+				t.Errorf("unable to reset database: %v", err)
+			}
 		}
 	}
 	if err != nil {

From 699bd4207dbbe26b0c2624e7ccb3d7293d2be324 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Fri, 16 Jul 2021 18:17:31 +0100
Subject: [PATCH 41/44] Add basic edit ldap auth test & actually fix #16252

One of the reasons why #16447 was needed and why #16268 was needed in
the first place was because it appears that editing ldap configuration
doesn't get tested.

This PR therefore adds a basic test that will run the edit pipeline.

In doing so it's now clear that #16447 and #16268 aren't actually
solving #16252. It turns out that what actually happens is that is that
the bytes are actually double encoded.

This PR now changes the json unmarshal wrapper to handle this double
encode.

Fix #16252

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 integrations/auth_ldap_test.go | 54 ++++++++++++++++++++++++++++++++++
 models/login_source.go         | 36 ++++++++++++++++++-----
 models/repo_unit.go            | 10 +++----
 3 files changed, 87 insertions(+), 13 deletions(-)

diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go
index 4d82c092e7280..59f51951234e0 100644
--- a/integrations/auth_ldap_test.go
+++ b/integrations/auth_ldap_test.go
@@ -144,6 +144,60 @@ func TestLDAPUserSignin(t *testing.T) {
 	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
 }
 
+func TestLDAPAuthChange(t *testing.T) {
+	defer prepareTestEnv(t)()
+	addAuthSourceLDAP(t, "")
+
+	session := loginUser(t, "user1")
+	req := NewRequest(t, "GET", "/admin/auths")
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+	href, exists := doc.Find("table.table td a").Attr("href")
+	if !exists {
+		assert.True(t, exists, "No authentication source found")
+		return
+	}
+
+	req = NewRequest(t, "GET", href)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	doc = NewHTMLParser(t, resp.Body)
+	csrf := doc.GetCSRF()
+	host, _ := doc.Find(`input[name="host"]`).Attr("value")
+	assert.Equal(t, host, getLDAPServerHost())
+	binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
+	assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
+
+	req = NewRequestWithValues(t, "POST", href, map[string]string{
+		"_csrf":                    csrf,
+		"type":                     "2",
+		"name":                     "ldap",
+		"host":                     getLDAPServerHost(),
+		"port":                     "389",
+		"bind_dn":                  "uid=gitea,ou=service,dc=planetexpress,dc=com",
+		"bind_password":            "password",
+		"user_base":                "ou=people,dc=planetexpress,dc=com",
+		"filter":                   "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))",
+		"admin_filter":             "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
+		"restricted_filter":        "(uid=leela)",
+		"attribute_username":       "uid",
+		"attribute_name":           "givenName",
+		"attribute_surname":        "sn",
+		"attribute_mail":           "mail",
+		"attribute_ssh_public_key": "",
+		"is_sync_enabled":          "on",
+		"is_active":                "on",
+	})
+	session.MakeRequest(t, req, http.StatusFound)
+
+	req = NewRequest(t, "GET", href)
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	doc = NewHTMLParser(t, resp.Body)
+	host, _ = doc.Find(`input[name="host"]`).Attr("value")
+	assert.Equal(t, host, getLDAPServerHost())
+	binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value")
+	assert.Equal(t, binddn, "uid=gitea,ou=service,dc=planetexpress,dc=com")
+}
+
 func TestLDAPUserSync(t *testing.T) {
 	if skipLDAPTests() {
 		t.Skip()
diff --git a/models/login_source.go b/models/login_source.go
index bbd605bb41d7d..5674196e0c66e 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -7,6 +7,7 @@ package models
 
 import (
 	"crypto/tls"
+	"encoding/binary"
 	"errors"
 	"fmt"
 	"net/smtp"
@@ -70,11 +71,30 @@ var (
 	_ convert.Conversion = &SSPIConfig{}
 )
 
-// jsonUnmarshalIgnoreErroneousBOM - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
-// possible that a Blob may gain an unwanted prefix of 0xff 0xfe.
-func jsonUnmarshalIgnoreErroneousBOM(bs []byte, v interface{}) error {
+// jsonUnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
+// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
+func jsonUnmarshalHandleDoubleEncode(bs []byte, v interface{}) error {
 	json := jsoniter.ConfigCompatibleWithStandardLibrary
 	err := json.Unmarshal(bs, v)
+	if err != nil {
+		ok := true
+		rs := []byte{}
+		temp := make([]byte, 2)
+		for _, rn := range string(bs) {
+			if rn > 0xffff {
+				ok = false
+				break
+			}
+			binary.LittleEndian.PutUint16(temp, uint16(rn))
+			rs = append(rs, temp...)
+		}
+		if ok {
+			if rs[0] == 0xff && rs[1] == 0xfe {
+				rs = rs[2:]
+			}
+			err = json.Unmarshal(rs, v)
+		}
+	}
 	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
 		err = json.Unmarshal(bs[2:], v)
 	}
@@ -88,7 +108,7 @@ type LDAPConfig struct {
 
 // FromDB fills up a LDAPConfig from serialized format.
 func (cfg *LDAPConfig) FromDB(bs []byte) error {
-	err := jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	err := jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 	if err != nil {
 		return err
 	}
@@ -129,7 +149,7 @@ type SMTPConfig struct {
 
 // FromDB fills up an SMTPConfig from serialized format.
 func (cfg *SMTPConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
@@ -146,7 +166,7 @@ type PAMConfig struct {
 
 // FromDB fills up a PAMConfig from serialized format.
 func (cfg *PAMConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 }
 
 // ToDB exports a PAMConfig to a serialized format.
@@ -167,7 +187,7 @@ type OAuth2Config struct {
 
 // FromDB fills up an OAuth2Config from serialized format.
 func (cfg *OAuth2Config) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
@@ -187,7 +207,7 @@ type SSPIConfig struct {
 
 // FromDB fills up an SSPIConfig from serialized format.
 func (cfg *SSPIConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
 }
 
 // ToDB exports an SSPIConfig to a serialized format.
diff --git a/models/repo_unit.go b/models/repo_unit.go
index a12e056a7d5ad..f430e4f7f3bf0 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -28,7 +28,7 @@ type UnitConfig struct{}
 
 // FromDB fills up a UnitConfig from serialized format.
 func (cfg *UnitConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a UnitConfig to a serialized format.
@@ -44,7 +44,7 @@ type ExternalWikiConfig struct {
 
 // FromDB fills up a ExternalWikiConfig from serialized format.
 func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a ExternalWikiConfig to a serialized format.
@@ -62,7 +62,7 @@ type ExternalTrackerConfig struct {
 
 // FromDB fills up a ExternalTrackerConfig from serialized format.
 func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a ExternalTrackerConfig to a serialized format.
@@ -80,7 +80,7 @@ type IssuesConfig struct {
 
 // FromDB fills up a IssuesConfig from serialized format.
 func (cfg *IssuesConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a IssuesConfig to a serialized format.
@@ -104,7 +104,7 @@ type PullRequestsConfig struct {
 
 // FromDB fills up a PullRequestsConfig from serialized format.
 func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalIgnoreErroneousBOM(bs, &cfg)
+	return jsonUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a PullRequestsConfig to a serialized format.

From 70b197538b1afbd9be962ae6b420e65fceb26e75 Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 17 Jul 2021 09:36:34 +0100
Subject: [PATCH 42/44] fixup! Merge branch 'add-ldap-configuration-edit-tests'
 into move-login-out-of-models

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 services/auth/source/ldap/source.go   | 2 +-
 services/auth/source/oauth2/source.go | 2 +-
 services/auth/source/smtp/source.go   | 2 +-
 services/auth/source/sspi/source.go   | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index 58efc9b29fd88..87be0117ee5d8 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -60,7 +60,7 @@ type Source struct {
 
 // FromDB fills up a LDAPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	err := models.JSONUnmarshalHandleDoubleEncode(bs, source)
+	err := models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 	if err != nil {
 		return err
 	}
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 8bbb39df67943..e9c49ef90b8ac 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -32,7 +32,7 @@ type Source struct {
 
 // FromDB fills up an OAuth2Config from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	return models.JSONUnmarshalHandleDoubleEncode(bs, source)
+	return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 0333e1e6f9e1c..0f948d5381897 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -32,7 +32,7 @@ type Source struct {
 
 // FromDB fills up an SMTPConfig from serialized format.
 func (source *Source) FromDB(bs []byte) error {
-	return models.JSONUnmarshalHandleDoubleEncode(bs, source)
+	return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
 }
 
 // ToDB exports an SMTPConfig to a serialized format.
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
index c10ac0e7aa5b6..e4be446f30dbe 100644
--- a/services/auth/source/sspi/source.go
+++ b/services/auth/source/sspi/source.go
@@ -27,7 +27,7 @@ type Source struct {
 
 // FromDB fills up an SSPIConfig from serialized format.
 func (cfg *Source) FromDB(bs []byte) error {
-	return models.JSONUnmarshalHandleDoubleEncode(bs, cfg)
+	return models.JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports an SSPIConfig to a serialized format.

From 116f109021b94e0dd51dd66c64d4f5346e8b401d Mon Sep 17 00:00:00 2001
From: Andrew Thornton <art27@cantab.net>
Date: Sat, 17 Jul 2021 16:04:36 +0100
Subject: [PATCH 43/44] fix edit template

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 templates/admin/auth/edit.tmpl | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 01c4b57395c9d..3fbfedefe7012 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -22,7 +22,7 @@
 
 				<!-- LDAP and DLDAP -->
 				{{if or .Source.IsLDAP .Source.IsDLDAP}}
-					{{ $cfg:=.Source.LDAP }}
+					{{ $cfg:=.Source.Cfg }}
 					<div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}">
 						<label>{{.i18n.Tr "admin.auths.security_protocol"}}</label>
 						<div class="ui selection security-protocol dropdown">
@@ -151,7 +151,7 @@
 
 				<!-- SMTP -->
 				{{if .Source.IsSMTP}}
-					{{ $cfg:=.Source.SMTP }}
+					{{ $cfg:=.Source.Cfg }}
 					<div class="inline required field">
 						<label>{{.i18n.Tr "admin.auths.smtp_auth"}}</label>
 						<div class="ui selection type dropdown">
@@ -182,7 +182,7 @@
 
 				<!-- PAM -->
 				{{if .Source.IsPAM}}
-					{{ $cfg:=.Source.PAM }}
+					{{ $cfg:=.Source.Cfg }}
 					<div class="required field">
 						<label for="pam_service_name">{{.i18n.Tr "admin.auths.pam_service_name"}}</label>
 						<input id="pam_service_name" name="pam_service_name" value="{{$cfg.ServiceName}}" required>
@@ -195,7 +195,7 @@
 
 				<!-- OAuth2 -->
 				{{if .Source.IsOAuth2}}
-					{{ $cfg:=.Source.OAuth2 }}
+					{{ $cfg:=.Source.Cfg }}
 					<div class="inline required field">
 						<label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
 						<div class="ui selection type dropdown">
@@ -258,7 +258,7 @@
 
 				<!-- SSPI -->
 				{{if .Source.IsSSPI}}
-					{{ $cfg:=.Source.SSPI }}
+					{{ $cfg:=.Source.Cfg }}
 					<div class="field">
 						<div class="ui checkbox">
 							<label for="sspi_auto_create_users"><strong>{{.i18n.Tr "admin.auths.sspi_auto_create_users"}}</strong></label>

From 66cb90f93d6d505034a1debffb8eb36d4026b5b1 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Sat, 17 Jul 2021 20:53:56 +0200
Subject: [PATCH 44/44] sort import

---
 cmd/admin.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/cmd/admin.go b/cmd/admin.go
index 69c8a5669af5d..94e78186c902f 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -14,8 +14,6 @@ import (
 	"text/tabwriter"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/services/auth/source/oauth2"
-
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
@@ -23,6 +21,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
+	"code.gitea.io/gitea/services/auth/source/oauth2"
 
 	"github.com/urfave/cli"
 )