From 5d2e11eedb837f26d13e3b904583730cd8492fbd Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Sat, 24 Jul 2021 11:16:34 +0100
Subject: [PATCH] Refactor: Move login out of models (#16199)

`models` does far too much. In particular it handles all `UserSignin`.

It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in.

Therefore we should move this code out of `models`.

This code has to depend on `models` - therefore it belongs in `services`.

There is a package in `services` called `auth` and clearly this functionality belongs in there.

Plan:

- [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication.
- [x] Move `models.UserSignIn` into `auth`
- [x] Move `models.ExternalUserLogin`
- [x] Move most of the `LoginVia*` methods to `auth` or subpackages
- [x] Move Resynchronize functionality to `auth`
  - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files.
- [x] Move the rest of the LDAP functionality in to the ldap subpackage
- [x] Re-factor the login sources to express an interfaces `auth.Source`?
  - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future
- [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable
- [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2
  - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models.
  - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2
- [x] More simplifications of login_source.go may need to be done
- Allow wiring in of notify registration -  *this can now easily be done - but I think we should do it in another PR*  - see #16178
- More refactors...?
  - OpenID should probably become an auth Method but I think that can be left for another PR
  - Methods should also probably be cleaned up  - again another PR I think.
  - SSPI still needs more refactors.* Rename auth.Auth auth.Method
* 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>
---
 cmd/admin.go                                  |   18 +-
 cmd/admin_auth_ldap.go                        |   76 +-
 cmd/admin_auth_ldap_test.go                   |  481 +++----
 integrations/auth_ldap_test.go                |    6 +-
 models/helper.go                              |   36 +
 models/login_source.go                        |  773 ++----------
 .../Test_unwrapLDAPSourceCfg/login_source.yml |   48 +
 models/migrations/migrations.go               |    2 +
 models/migrations/migrations_test.go          |    3 +
 models/migrations/v189.go                     |  111 ++
 models/migrations/v189_test.go                |   83 ++
 models/oauth2.go                              |  154 +--
 models/oauth2_application.go                  |   77 --
 models/repo_unit.go                           |   10 +-
 models/ssh_key.go                             | 1116 ++---------------
 models/ssh_key_authorized_keys.go             |  219 ++++
 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/store.go                               |   16 +
 models/user.go                                |  341 +----
 models/user_test.go                           |    4 +-
 modules/context/api.go                        |    2 +-
 modules/context/context.go                    |    2 +-
 modules/cron/tasks_basic.go                   |    3 +-
 routers/init.go                               |    3 +-
 routers/web/admin/auths.go                    |  123 +-
 routers/web/user/auth.go                      |   22 +-
 routers/web/user/auth_openid.go               |    3 +-
 routers/web/user/oauth.go                     |   14 +-
 routers/web/user/setting/account.go           |    3 +-
 routers/web/user/setting/security.go          |    5 +-
 services/auth/auth.go                         |   20 +-
 services/auth/basic.go                        |   15 +-
 services/auth/group.go                        |   34 +-
 services/auth/interface.go                    |   45 +-
 services/auth/oauth2.go                       |   18 +-
 services/auth/reverseproxy.go                 |   13 +-
 services/auth/session.go                      |   13 +-
 services/auth/signin.go                       |  113 ++
 .../auth/source/db/assert_interface_test.go   |   21 +
 services/auth/source/db/authenticate.go       |   42 +
 services/auth/source/db/source.go             |   31 +
 .../auth/source}/ldap/README.md               |   77 +-
 .../auth/source/ldap/assert_interface_test.go |   27 +
 .../auth/source/ldap/security_protocol.go     |   27 +
 services/auth/source/ldap/source.go           |  120 ++
 .../auth/source/ldap/source_authenticate.go   |   93 ++
 .../auth/source/ldap/source_search.go         |   43 -
 services/auth/source/ldap/source_sync.go      |  184 +++
 services/auth/source/ldap/util.go             |   19 +
 .../source/oauth2/assert_interface_test.go    |   23 +
 services/auth/source/oauth2/init.go           |   83 ++
 .../auth/source}/oauth2/jwtsigningkey.go      |    0
 .../auth/source/oauth2/providers.go           |  202 ++-
 services/auth/source/oauth2/source.go         |   51 +
 .../auth/source/oauth2/source_authenticate.go |   15 +
 services/auth/source/oauth2/source_callout.go |   42 +
 .../auth/source/oauth2/source_register.go     |   30 +
 services/auth/source/oauth2/token.go          |   94 ++
 services/auth/source/oauth2/urlmapping.go     |   24 +
 .../auth/source/pam/assert_interface_test.go  |   22 +
 services/auth/source/pam/source.go            |   47 +
 .../auth/source/pam/source_authenticate.go    |   62 +
 .../auth/source/smtp/assert_interface_test.go |   25 +
 services/auth/source/smtp/auth.go             |   81 ++
 services/auth/source/smtp/source.go           |   66 +
 .../auth/source/smtp/source_authenticate.go   |   71 ++
 .../auth/source/sspi/assert_interface_test.go |   19 +
 services/auth/source/sspi/source.go           |   41 +
 services/auth/sspi_windows.go                 |   16 +-
 services/auth/sync.go                         |   43 +
 templates/admin/auth/edit.tmpl                |   12 +-
 templates/admin/auth/list.tmpl                |    2 +-
 .../user/settings/security_accountlinks.tmpl  |    2 +-
 77 files changed, 3803 insertions(+), 2951 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
 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
 create mode 100644 models/store.go
 create mode 100644 services/auth/signin.go
 create mode 100644 services/auth/source/db/assert_interface_test.go
 create mode 100644 services/auth/source/db/authenticate.go
 create mode 100644 services/auth/source/db/source.go
 rename {modules/auth => services/auth/source}/ldap/README.md (57%)
 create mode 100644 services/auth/source/ldap/assert_interface_test.go
 create mode 100644 services/auth/source/ldap/security_protocol.go
 create mode 100644 services/auth/source/ldap/source.go
 create mode 100644 services/auth/source/ldap/source_authenticate.go
 rename modules/auth/ldap/ldap.go => services/auth/source/ldap/source_search.go (85%)
 create mode 100644 services/auth/source/ldap/source_sync.go
 create mode 100644 services/auth/source/ldap/util.go
 create mode 100644 services/auth/source/oauth2/assert_interface_test.go
 create mode 100644 services/auth/source/oauth2/init.go
 rename {modules/auth => services/auth/source}/oauth2/jwtsigningkey.go (100%)
 rename modules/auth/oauth2/oauth2.go => services/auth/source/oauth2/providers.go (61%)
 create mode 100644 services/auth/source/oauth2/source.go
 create mode 100644 services/auth/source/oauth2/source_authenticate.go
 create mode 100644 services/auth/source/oauth2/source_callout.go
 create mode 100644 services/auth/source/oauth2/source_register.go
 create mode 100644 services/auth/source/oauth2/token.go
 create mode 100644 services/auth/source/oauth2/urlmapping.go
 create mode 100644 services/auth/source/pam/assert_interface_test.go
 create mode 100644 services/auth/source/pam/source.go
 create mode 100644 services/auth/source/pam/source_authenticate.go
 create mode 100644 services/auth/source/smtp/assert_interface_test.go
 create mode 100644 services/auth/source/smtp/auth.go
 create mode 100644 services/auth/source/smtp/source.go
 create mode 100644 services/auth/source/smtp/source_authenticate.go
 create mode 100644 services/auth/source/sspi/assert_interface_test.go
 create mode 100644 services/auth/source/sspi/source.go
 create mode 100644 services/auth/sync.go

diff --git a/cmd/admin.go b/cmd/admin.go
index f58a1f996..94e78186c 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -14,7 +14,6 @@ import (
 	"text/tabwriter"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/oauth2"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
@@ -22,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"
 )
@@ -597,7 +597,7 @@ func runRegenerateKeys(_ *cli.Context) error {
 	return models.RewriteAllPublicKeys()
 }
 
-func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
+func parseOAuth2Config(c *cli.Context) *oauth2.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if c.IsSet("use-custom-urls") {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -609,7 +609,7 @@ func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
 	} else {
 		customURLMapping = nil
 	}
-	return &models.OAuth2Config{
+	return &oauth2.Source{
 		Provider:                      c.String("provider"),
 		ClientID:                      c.String("key"),
 		ClientSecret:                  c.String("secret"),
@@ -625,10 +625,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),
 	})
 }
 
@@ -646,7 +646,7 @@ func runUpdateOauth(c *cli.Context) error {
 		return err
 	}
 
-	oAuth2Config := source.OAuth2()
+	oAuth2Config := source.Cfg.(*oauth2.Source)
 
 	if c.IsSet("name") {
 		source.Name = c.String("name")
@@ -728,7 +728,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 5ab64ec7d..4314930a3 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
+	"code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/urfave/cli"
 )
@@ -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")
@@ -180,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 *models.LDAPConfig) 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
 }
@@ -251,7 +251,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
 		}
@@ -289,17 +289,15 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
 	}
 
 	loginSource := &models.LoginSource{
-		Type:      models.LoginLDAP,
-		IsActived: true, // active by default
-		Cfg: &models.LDAPConfig{
-			Source: &ldap.Source{
-				Enabled: true, // always true
-			},
+		Type:     models.LoginLDAP,
+		IsActive: true, // active by default
+		Cfg: &ldap.Source{
+			Enabled: true, // always true
 		},
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -318,7 +316,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.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -336,17 +334,15 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 	}
 
 	loginSource := &models.LoginSource{
-		Type:      models.LoginDLDAP,
-		IsActived: true, // active by default
-		Cfg: &models.LDAPConfig{
-			Source: &ldap.Source{
-				Enabled: true, // always true
-			},
+		Type:     models.LoginDLDAP,
+		IsActive: true, // active by default
+		Cfg: &ldap.Source{
+			Enabled: true, // always true
 		},
 	}
 
 	parseLoginSource(c, loginSource)
-	if err := parseLdapConfig(c, loginSource.LDAP()); err != nil {
+	if err := parseLdapConfig(c, loginSource.Cfg.(*ldap.Source)); err != nil {
 		return err
 	}
 
@@ -365,7 +361,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.(*ldap.Source)); err != nil {
 		return err
 	}
 
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index 87f4f789a..692b11e3f 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/auth/ldap"
+	"code.gitea.io/gitea/services/auth/source/ldap"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/urfave/cli"
@@ -54,30 +54,28 @@ 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: &models.LDAPConfig{
-					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,
 				},
 			},
 		},
@@ -94,20 +92,18 @@ 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,
-				Cfg: &models.LDAPConfig{
-					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,
-					},
+				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",
+					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,
 				},
 			},
 		},
@@ -276,28 +272,26 @@ 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,
-				Cfg: &models.LDAPConfig{
-					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,
-					},
+				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",
+					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,
 				},
 			},
 		},
@@ -314,20 +308,18 @@ 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,
-				Cfg: &models.LDAPConfig{
-					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,
-					},
+				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",
+					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,
 				},
 			},
 		},
@@ -516,41 +508,37 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			id: 23,
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: true,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Enabled: true,
-					},
+				Type:     models.LoginLDAP,
+				IsActive: true,
+				Cfg: &ldap.Source{
+					Enabled: true,
 				},
 			},
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				Name:          "ldap (via Bind DN) source full",
-				IsActived:     false,
+				IsActive:      false,
 				IsSyncEnabled: true,
-				Cfg: &models.LDAPConfig{
-					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,
 				},
 			},
 		},
@@ -562,9 +550,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 		},
 		// case 2
@@ -577,10 +563,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
 				Name: "ldap (via Bind DN) source",
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Name: "ldap (via Bind DN) source",
-					},
+				Cfg: &ldap.Source{
+					Name: "ldap (via Bind DN) source",
 				},
 			},
 		},
@@ -592,18 +576,14 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				"--not-active",
 			},
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: true,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Type:     models.LoginLDAP,
+				IsActive: true,
+				Cfg:      &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginLDAP,
-				IsActived: false,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Type:     models.LoginLDAP,
+				IsActive: false,
+				Cfg:      &ldap.Source{},
 			},
 		},
 		// case 4
@@ -615,10 +595,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						SecurityProtocol: ldap.SecurityProtocol(1),
-					},
+				Cfg: &ldap.Source{
+					SecurityProtocol: ldap.SecurityProtocol(1),
 				},
 			},
 		},
@@ -631,10 +609,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						SkipVerify: true,
-					},
+				Cfg: &ldap.Source{
+					SkipVerify: true,
 				},
 			},
 		},
@@ -647,10 +623,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Host: "ldap-server",
-					},
+				Cfg: &ldap.Source{
+					Host: "ldap-server",
 				},
 			},
 		},
@@ -663,10 +637,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Port: 389,
-					},
+				Cfg: &ldap.Source{
+					Port: 389,
 				},
 			},
 		},
@@ -679,10 +651,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						UserBase: "ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserBase: "ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -695,10 +665,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					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)",
 				},
 			},
 		},
@@ -711,10 +679,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					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)",
 				},
 			},
 		},
@@ -727,10 +693,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeUsername: "uid",
-					},
+				Cfg: &ldap.Source{
+					AttributeUsername: "uid",
 				},
 			},
 		},
@@ -743,10 +707,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeName: "givenName",
-					},
+				Cfg: &ldap.Source{
+					AttributeName: "givenName",
 				},
 			},
 		},
@@ -759,10 +721,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeSurname: "sn",
-					},
+				Cfg: &ldap.Source{
+					AttributeSurname: "sn",
 				},
 			},
 		},
@@ -775,10 +735,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeMail: "mail",
-					},
+				Cfg: &ldap.Source{
+					AttributeMail: "mail",
 				},
 			},
 		},
@@ -791,10 +749,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributesInBind: true,
-					},
+				Cfg: &ldap.Source{
+					AttributesInBind: true,
 				},
 			},
 		},
@@ -807,10 +763,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeSSHPublicKey: "publickey",
-					},
+				Cfg: &ldap.Source{
+					AttributeSSHPublicKey: "publickey",
 				},
 			},
 		},
@@ -823,10 +777,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						BindDN: "cn=readonly,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					BindDN: "cn=readonly,dc=domain,dc=org",
 				},
 			},
 		},
@@ -839,10 +791,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						BindPassword: "secret",
-					},
+				Cfg: &ldap.Source{
+					BindPassword: "secret",
 				},
 			},
 		},
@@ -856,9 +806,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type:          models.LoginLDAP,
 				IsSyncEnabled: true,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Cfg:           &ldap.Source{},
 			},
 		},
 		// case 20
@@ -870,10 +818,8 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						SearchPageSize: 12,
-					},
+				Cfg: &ldap.Source{
+					SearchPageSize: 12,
 				},
 			},
 		},
@@ -901,9 +847,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginOAuth2,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 			errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 		},
@@ -933,9 +877,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginLDAP,
-					Cfg: &models.LDAPConfig{
-						Source: &ldap.Source{},
-					},
+					Cfg:  &ldap.Source{},
 				}, nil
 			},
 		}
@@ -994,27 +936,25 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			id: 7,
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				Name:      "ldap (simple auth) source full",
-				IsActived: false,
-				Cfg: &models.LDAPConfig{
-					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)",
-					},
+				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",
+					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)",
 				},
 			},
 		},
@@ -1026,9 +966,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 		},
 		// case 2
@@ -1041,10 +979,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
 				Name: "ldap (simple auth) source",
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Name: "ldap (simple auth) source",
-					},
+				Cfg: &ldap.Source{
+					Name: "ldap (simple auth) source",
 				},
 			},
 		},
@@ -1056,18 +992,14 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				"--not-active",
 			},
 			existingLoginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				IsActived: true,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Type:     models.LoginDLDAP,
+				IsActive: true,
+				Cfg:      &ldap.Source{},
 			},
 			loginSource: &models.LoginSource{
-				Type:      models.LoginDLDAP,
-				IsActived: false,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Type:     models.LoginDLDAP,
+				IsActive: false,
+				Cfg:      &ldap.Source{},
 			},
 		},
 		// case 4
@@ -1079,10 +1011,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						SecurityProtocol: ldap.SecurityProtocol(2),
-					},
+				Cfg: &ldap.Source{
+					SecurityProtocol: ldap.SecurityProtocol(2),
 				},
 			},
 		},
@@ -1095,10 +1025,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						SkipVerify: true,
-					},
+				Cfg: &ldap.Source{
+					SkipVerify: true,
 				},
 			},
 		},
@@ -1111,10 +1039,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Host: "ldap-server",
-					},
+				Cfg: &ldap.Source{
+					Host: "ldap-server",
 				},
 			},
 		},
@@ -1127,10 +1053,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Port: 987,
-					},
+				Cfg: &ldap.Source{
+					Port: 987,
 				},
 			},
 		},
@@ -1143,10 +1067,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						UserBase: "ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserBase: "ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -1159,10 +1081,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						Filter: "(&(objectClass=posixAccount)(cn=%s))",
-					},
+				Cfg: &ldap.Source{
+					Filter: "(&(objectClass=posixAccount)(cn=%s))",
 				},
 			},
 		},
@@ -1175,10 +1095,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					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)",
 				},
 			},
 		},
@@ -1191,10 +1109,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeUsername: "uid",
-					},
+				Cfg: &ldap.Source{
+					AttributeUsername: "uid",
 				},
 			},
 		},
@@ -1207,10 +1123,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeName: "givenName",
-					},
+				Cfg: &ldap.Source{
+					AttributeName: "givenName",
 				},
 			},
 		},
@@ -1223,10 +1137,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeSurname: "sn",
-					},
+				Cfg: &ldap.Source{
+					AttributeSurname: "sn",
 				},
 			},
 		},
@@ -1239,10 +1151,9 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeMail: "mail",
-					},
+				Cfg: &ldap.Source{
+
+					AttributeMail: "mail",
 				},
 			},
 		},
@@ -1255,10 +1166,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						AttributeSSHPublicKey: "publickey",
-					},
+				Cfg: &ldap.Source{
+					AttributeSSHPublicKey: "publickey",
 				},
 			},
 		},
@@ -1271,10 +1180,8 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			loginSource: &models.LoginSource{
 				Type: models.LoginDLDAP,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{
-						UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
-					},
+				Cfg: &ldap.Source{
+					UserDN: "cn=%s,ou=Users,dc=domain,dc=org",
 				},
 			},
 		},
@@ -1302,9 +1209,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 			},
 			existingLoginSource: &models.LoginSource{
 				Type: models.LoginPAM,
-				Cfg: &models.LDAPConfig{
-					Source: &ldap.Source{},
-				},
+				Cfg:  &ldap.Source{},
 			},
 			errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 		},
@@ -1334,9 +1239,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 				}
 				return &models.LoginSource{
 					Type: models.LoginDLDAP,
-					Cfg: &models.LDAPConfig{
-						Source: &ldap.Source{},
-					},
+					Cfg:  &ldap.Source{},
 				}, nil
 			},
 		}
diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go
index 59f519512..6eb017017 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"
@@ -205,7 +205,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
@@ -270,7 +270,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/helper.go b/models/helper.go
index 91063b2d1..798fa3fef 100644
--- a/models/helper.go
+++ b/models/helper.go
@@ -4,6 +4,12 @@
 
 package models
 
+import (
+	"encoding/binary"
+
+	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 +33,33 @@ func valuesUser(m map[int64]*User) []*User {
 	}
 	return values
 }
+
+// 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)
+	}
+	return err
+}
diff --git a/models/login_source.go b/models/login_source.go
index 5674196e0..5e1c6e222 100644
--- a/models/login_source.go
+++ b/models/login_source.go
@@ -6,25 +6,11 @@
 package models
 
 import (
-	"crypto/tls"
-	"encoding/binary"
-	"errors"
-	"fmt"
-	"net/smtp"
-	"net/textproto"
+	"reflect"
 	"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"
 	"xorm.io/xorm/convert"
@@ -45,6 +31,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)",
@@ -55,173 +46,66 @@ 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{}
-)
+// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
+type SkipVerifiable interface {
+	IsSkipVerify() bool
+}
 
-// 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)
+// HasTLSer configurations provide a HasTLS to check if TLS can be enabled
+type HasTLSer interface {
+	HasTLS() bool
+}
+
+// UseTLSer configurations provide a HasTLS to check if TLS is enabled
+type UseTLSer interface {
+	UseTLS() bool
+}
+
+// SSHKeyProvider configurations provide ProvidesSSHKeys to check if they provide SSHKeys
+type SSHKeyProvider interface {
+	ProvidesSSHKeys() bool
+}
+
+// RegisterableSource configurations provide RegisterSource which needs to be run on creation
+type RegisterableSource interface {
+	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
+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
 	}
-	if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
-		err = json.Unmarshal(bs[2:], v)
+
+	// Not a Pointer
+	registeredLoginConfigs[typ] = func() LoginConfig {
+		return reflect.New(reflect.TypeOf(exemplar)).Elem().Interface().(LoginConfig)
 	}
-	return err
 }
 
-// LDAPConfig holds configuration for LDAP login source.
-type LDAPConfig struct {
-	*ldap.Source
-}
-
-// FromDB fills up a LDAPConfig from serialized format.
-func (cfg *LDAPConfig) FromDB(bs []byte) error {
-	err := jsonUnmarshalHandleDoubleEncode(bs, &cfg)
-	if err != nil {
-		return err
-	}
-	if cfg.BindPasswordEncrypt != "" {
-		cfg.BindPassword, err = secret.DecryptSecret(setting.SecretKey, cfg.BindPasswordEncrypt)
-		cfg.BindPasswordEncrypt = ""
-	}
-	return err
-}
-
-// 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
-}
-
-// FromDB fills up an SMTPConfig from serialized format.
-func (cfg *SMTPConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalHandleDoubleEncode(bs, cfg)
-}
-
-// ToDB exports an SMTPConfig to a serialized format.
-func (cfg *SMTPConfig) ToDB() ([]byte, error) {
-	json := jsoniter.ConfigCompatibleWithStandardLibrary
-	return json.Marshal(cfg)
-}
-
-// PAMConfig holds configuration for the PAM login source.
-type PAMConfig struct {
-	ServiceName string // pam service (e.g. system-auth)
-	EmailDomain string
-}
-
-// FromDB fills up a PAMConfig from serialized format.
-func (cfg *PAMConfig) FromDB(bs []byte) error {
-	return jsonUnmarshalHandleDoubleEncode(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 {
-	return jsonUnmarshalHandleDoubleEncode(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 {
-	return jsonUnmarshalHandleDoubleEncode(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]func() LoginConfig{}
 
 // LoginSource represents an external way for authorizing users.
 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"`
 
@@ -245,19 +129,14 @@ 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))
+		constructor, ok := registeredLoginConfigs[typ]
+		if !ok {
+			return
+		}
+		source.Cfg = constructor()
+		if settable, ok := source.Cfg.(LoginSourceSettable); ok {
+			settable.SetLoginSource(source)
 		}
 	}
 }
@@ -299,59 +178,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
@@ -369,16 +210,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.IsActive {
+		return nil
+	}
+
+	registerableSource, ok := source.Cfg.(RegisterableSource)
+	if !ok {
+		return nil
+	}
+
+	err = registerableSource.RegisterSource()
+	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
@@ -399,10 +248,19 @@ 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_active = ?", 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 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
@@ -425,6 +283,14 @@ 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]()
+		// Set this source to active
+		// FIXME: allow disabling of db based password authentication in future
+		source.IsActive = true
+		return source, nil
+	}
+
 	has, err := x.ID(id).Get(source)
 	if err != nil {
 		return nil, err
@@ -446,16 +312,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.IsActive {
+		return nil
+	}
+
+	registerableSource, ok := source.Cfg.(RegisterableSource)
+	if !ok {
+		return nil
+	}
+
+	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 {
+			log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 		}
 	}
 	return err
@@ -477,8 +351,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(); err != nil {
+			return err
+		}
 	}
 
 	_, err = x.ID(source.ID).Delete(new(LoginSource))
@@ -490,404 +366,3 @@ func CountLoginSources() int64 {
 	count, _ := x.Count(new(LoginSource))
 	return count
 }
-
-// .____     ________      _____ __________
-// |    |    \______ \    /  _  \\______   \
-// |    |     |    |  \  /  /_\  \|     ___/
-// |    |___  |    `   \/    |    \    |
-// |_______ \/_______  /\____|__  /____|
-//         \/        \/         \/
-
-func composeFullName(firstname, surname, username string) string {
-	switch {
-	case len(firstname) == 0 && len(surname) == 0:
-		return username
-	case len(firstname) == 0:
-		return surname
-	case len(surname) == 0:
-		return firstname
-	default:
-		return firstname + " " + surname
-	}
-}
-
-// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
-// and create a local user if success when enabled.
-func LoginViaLDAP(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)
-}
-
-// 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
-		}
-
-		if 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, ErrUserNotExist{user.ID, user.Name, 0}
-}
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 000000000..4b72ba145
--- /dev/null
+++ b/models/migrations/fixtures/Test_unwrapLDAPSourceCfg/login_source.yml
@@ -0,0 +1,48 @@
+# type LoginSource struct {
+#   ID        int64 `xorm:"pk autoincr"`
+#   Type      int
+#   Cfg       []byte `xorm:"TEXT"`
+#   Expected  []byte `xorm:"TEXT"`
+# }
+-
+  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/migrations.go b/models/migrations/migrations.go
index 7a4193199..fed7b909c 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/migrations_test.go b/models/migrations/migrations_test.go
index 26066580d..634bfc848 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 {
diff --git a/models/migrations/v189.go b/models/migrations/v189.go
new file mode 100644
index 000000000..42b996353
--- /dev/null
+++ b/models/migrations/v189.go
@@ -0,0 +1,111 @@
+// 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 (
+	"encoding/binary"
+	"fmt"
+
+	jsoniter "github.com/json-iterator/go"
+	"xorm.io/xorm"
+)
+
+func unwrapLDAPSourceCfg(x *xorm.Engine) error {
+	jsonUnmarshalHandleDoubleEncode := func(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)
+		}
+		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"`
+		IsActive  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 := jsonUnmarshalHandleDoubleEncode([]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
+				}
+			}
+		}
+	}
+
+	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
new file mode 100644
index 000000000..f4fe6dec3
--- /dev/null
+++ b/models/migrations/v189_test.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 migrations
+
+import (
+	"testing"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/stretchr/testify/assert"
+)
+
+// LoginSource represents an external way for authorizing users.
+type LoginSourceOriginalV189 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 *LoginSourceOriginalV189) TableName() string {
+	return "login_source"
+}
+
+func Test_unwrapLDAPSourceCfg(t *testing.T) {
+
+	// Prepare and load the testing database
+	x, deferable := prepareTestEnv(t, 0, new(LoginSourceOriginalV189))
+	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)
+		return
+	}
+
+	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
+		}
+
+		for _, source := range sources {
+			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)
+			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 46da60e02..127e8d760 100644
--- a/models/oauth2.go
+++ b/models/oauth2.go
@@ -4,89 +4,10 @@
 
 package models
 
-import (
-	"sort"
-
-	"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)
-	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
@@ -95,81 +16,10 @@ 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
 	}
 
 	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 {
-		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 {
-		oAuth2Config := source.OAuth2()
-		err := oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
-		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
-}
-
-// 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/oauth2_application.go b/models/oauth2_application.go
index 5a924763b..2aa9fbd3d 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 signed 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/repo_unit.go b/models/repo_unit.go
index f430e4f7f..c5eac2656 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 jsonUnmarshalHandleDoubleEncode(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 jsonUnmarshalHandleDoubleEncode(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 jsonUnmarshalHandleDoubleEncode(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 jsonUnmarshalHandleDoubleEncode(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 jsonUnmarshalHandleDoubleEncode(bs, &cfg)
+	return JSONUnmarshalHandleDoubleEncode(bs, &cfg)
 }
 
 // ToDB exports a PullRequestsConfig to a serialized format.
diff --git a/models/ssh_key.go b/models/ssh_key.go
index 12c7bc911..6cda4f165 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"
-)
-
-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) {
@@ -635,8 +205,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).
@@ -708,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
 		}
@@ -737,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
 	}
@@ -782,603 +344,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 util.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)
+	// Delete keys marked for deletion
+	var sshKeysNeedUpdate bool
+	for _, KeyToDelete := range keys {
+		key, err := searchPublicKeyByContentWithEngine(sess, KeyToDelete)
 		if err != nil {
-			return fmt.Errorf("GetRepositoryByID: %v", err)
+			log.Error("SearchPublicKeyByContent: %v", err)
+			continue
 		}
-		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 = deletePublicKeys(sess, key.ID); err != nil {
+			log.Error("deletePublicKeys: %v", err)
+			continue
 		}
+		sshKeysNeedUpdate = true
 	}
 
-	if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
-		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
+	if err := sess.Commit(); err != nil {
+		return false, 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
+	return sshKeysNeedUpdate, 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
-	}
-
-	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)
+// 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
+			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)
 				}
-				if content == email.Email {
-					return content, nil
-				}
-			}
-
-		case "username":
-			if content == user.Name {
-				return content, nil
+			} else {
+				log.Trace("AddPublicKeysBySource[%s]: Added Public SSH Key for user %s", sshKeyName, usr.Name)
+				sshKeysNeedUpdate = true
 			}
 		}
-	}
-
-	return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
-}
-
-// 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
+		if !found && err != nil {
+			log.Warn("AddPublicKeysBySource[%s]: Skipping invalid Public SSH Key for user %s: %v", s.Name, usr.Name, sshKey)
 		}
 	}
+	return sshKeysNeedUpdate
+}
 
-	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)
+// 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 {
-		return err
+		log.Error("synchronizePublicKeys[%s]: Error listing Public SSH Keys for user %s: %v", s.Name, usr.Name, 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
+	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)
 			}
 		}
 	}
 
-	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))
+
+	// 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
 	}
 
-	t.Close()
-	return util.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)
+	// 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)
+		}
 	}
 
-	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
-	}
-
-	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 000000000..5736477a0
--- /dev/null
+++ b/models/ssh_key_authorized_keys.go
@@ -0,0 +1,219 @@
+// 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
+
+// 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{}{
+		"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 util.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 000000000..f90ab267a
--- /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 util.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 000000000..3189bcf45
--- /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 000000000..96cc7d9c4
--- /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 000000000..a86b7de02
--- /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 000000000..3459e43c8
--- /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/store.go b/models/store.go
new file mode 100644
index 000000000..e8eba28fb
--- /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/models/user.go b/models/user.go
index f606da53d..a4f94999e 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"
 )
 
@@ -1484,6 +1483,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
@@ -1724,339 +1730,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
-}
-
-// 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
-		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("addLdapSSHPublicKeys[%s]: LDAP 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)
-				}
-			} else {
-				log.Trace("addLdapSSHPublicKeys[%s]: Added LDAP 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)
-		}
-	}
-	return sshKeysNeedUpdate
-}
-
-// 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)
-
-	// Get Public Keys from DB with current LDAP source
-	var giteaKeys []string
-	keys, err := ListPublicLdapSSHKeys(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)
-	}
-
-	for _, v := range keys {
-		giteaKeys = append(giteaKeys, v.OmitEmail())
-	}
-
-	// Get Public Keys from LDAP and skip duplicate keys
-	var ldapKeys []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)
-			}
-		}
-	}
-
-	// 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))
-		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))
-
-	// 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)
-		}
-	}
-	if addLdapSSHPublicKeys(usr, s, newLdapSSHKeys) {
-		sshKeysNeedUpdate = true
-	}
-
-	// Mark LDAP keys from DB that doesn't exist in LDAP 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)
-			giteaKeysToDelete = append(giteaKeysToDelete, giteaKey)
-		}
-	}
-
-	// Delete LDAP keys from DB that doesn't exist in LDAP
-	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)
-	}
-	if needUpd {
-		sshKeysNeedUpdate = true
-	}
-
-	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 34c465c58..a76bca0ed 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -453,8 +453,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/context/api.go b/modules/context/api.go
index 506824674..78d48e916 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 64f8b1208..8949dd714 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -627,7 +627,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/modules/cron/tasks_basic.go b/modules/cron/tasks_basic.go
index d4ac4f443..6c61d628c 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/init.go b/routers/init.go
index 3ee7c7357..27cd066b7 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -35,6 +35,7 @@ import (
 	web_routers "code.gitea.io/gitea/routers/web"
 	"code.gitea.io/gitea/services/archiver"
 	"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"
@@ -100,7 +101,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 a2f9ab0a5..20efd4a2a 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -11,8 +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"
 	"code.gitea.io/gitea/modules/context"
@@ -20,6 +18,11 @@ 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/source/ldap"
+	"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"
@@ -74,9 +77,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},
 	}
 )
 
@@ -88,15 +91,15 @@ 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
 	ctx.Data["AuthSources"] = authSources
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -105,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
 	}
@@ -113,45 +116,43 @@ func NewAuthSource(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplAuthNew)
 }
 
-func parseLDAPConfig(form forms.AuthenticationForm) *models.LDAPConfig {
+func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 	var pageSize uint32
 	if form.UsePagedSearch {
 		pageSize = uint32(form.SearchPageSize)
 	}
-	return &models.LDAPConfig{
-		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,
 	}
 }
 
-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,
@@ -161,7 +162,7 @@ func parseSMTPConfig(form forms.AuthenticationForm) *models.SMTPConfig {
 	}
 }
 
-func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
+func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 	var customURLMapping *oauth2.CustomURLMapping
 	if form.Oauth2UseCustomURL {
 		customURLMapping = &oauth2.CustomURLMapping{
@@ -173,7 +174,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *models.OAuth2Config {
 	} else {
 		customURLMapping = nil
 	}
-	return &models.OAuth2Config{
+	return &oauth2.Source{
 		Provider:                      form.Oauth2Provider,
 		ClientID:                      form.Oauth2Key,
 		ClientSecret:                  form.Oauth2Secret,
@@ -183,7 +184,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"))
@@ -198,7 +199,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,
@@ -215,12 +216,12 @@ 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"] = models.SMTPAuths
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	ctx.Data["SSPIAutoCreateUsers"] = true
 	ctx.Data["SSPIAutoActivateUsers"] = true
@@ -238,7 +239,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,
 		}
@@ -271,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 {
@@ -297,9 +298,9 @@ func EditAuthSource(ctx *context.Context) {
 	ctx.Data["PageIsAdminAuthentications"] = true
 
 	ctx.Data["SecurityProtocols"] = securityProtocols
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 	if err != nil {
@@ -310,7 +311,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"] = oauth2.Providers[source.Cfg.(*oauth2.Source).Provider]
 	}
 	ctx.HTML(http.StatusOK, tplAuthEdit)
 }
@@ -322,9 +323,9 @@ func EditAuthSourcePost(ctx *context.Context) {
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminAuthentications"] = true
 
-	ctx.Data["SMTPAuths"] = models.SMTPAuths
-	ctx.Data["OAuth2Providers"] = models.OAuth2Providers
-	ctx.Data["OAuth2DefaultCustomURLMappings"] = models.OAuth2DefaultCustomURLMappings
+	ctx.Data["SMTPAuths"] = smtp.Authenticators
+	ctx.Data["OAuth2Providers"] = oauth2.Providers
+	ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings
 
 	source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid"))
 	if err != nil {
@@ -346,7 +347,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,
 		}
@@ -364,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/routers/web/user/auth.go b/routers/web/user/auth.go
index 7a205853b..50b25d087 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"
@@ -27,6 +26,8 @@ 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/auth/source/oauth2"
 	"code.gitea.io/gitea/services/externalaccount"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
@@ -135,7 +136,7 @@ func SignIn(ctx *context.Context) {
 		return
 	}
 
-	orderedOAuth2Names, oauth2Providers, err := models.GetActiveOAuth2Providers()
+	orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -155,7 +156,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 := oauth2.GetActiveOAuth2Providers()
 	if err != nil {
 		ctx.ServerError("UserSignIn", err)
 		return
@@ -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)
@@ -577,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.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 		if strings.Contains(err.Error(), "no provider for ") {
-			if err = models.ResetOAuth2(); err != nil {
+			if err = oauth2.ResetOAuth2(); err != nil {
 				ctx.ServerError("SignIn", err)
 				return
 			}
-			if err = oauth2.Auth(loginSource.Name, ctx.Req, ctx.Resp); err != nil {
+			if err = loginSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
 				ctx.ServerError("SignIn", err)
 			}
 			return
@@ -631,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.OAuth2().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)
@@ -772,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.(*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)
@@ -901,7 +901,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 1a73a08c4..3e3da71ac 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/oauth.go b/routers/web/user/oauth.go
index 72295b444..7e108f6e7 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/account.go b/routers/web/user/setting/account.go
index b805db620..80b186262 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"
 )
@@ -228,7 +229,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/routers/web/user/setting/security.go b/routers/web/user/setting/security.go
index 7753c5c16..dd5d2a20c 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,8 +93,8 @@ func loadSecurityData(ctx *context.Context) {
 		if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil {
 			var providerDisplayName string
 			if loginSource.IsOAuth2() {
-				providerTechnicalName := loginSource.OAuth2().Provider
-				providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName
+				providerTechnicalName := loginSource.Cfg.(*oauth2.Source).Provider
+				providerDisplayName = oauth2.Providers[providerTechnicalName].DisplayName
 			} else {
 				providerDisplayName = loginSource.Name
 			}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 5492a8b74..11a8c6ed1 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)
 }
 
@@ -57,7 +57,12 @@ func Init() {
 	}
 	specialInit()
 	for _, method := range Methods() {
-		err := method.Init()
+		initializable, ok := method.(Initializable)
+		if !ok {
+			continue
+		}
+
+		err := initializable.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 0bce4f1d0..d492a52a6 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -19,7 +19,8 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Basic{}
+	_ 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.
@@ -116,7 +107,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/group.go b/services/auth/group.go
index b61949de7..fb885b818 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -12,30 +12,32 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Group{}
+	_ Method        = &Group{}
+	_ Initializable = &Group{}
+	_ Freeable      = &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,
 	}
 }
 
-// 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 a305bdfc2..51c704337 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"
@@ -18,18 +19,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 {
-	Name() string
-
-	// 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
-
-	// Free should be called exactly once before application closes, in order to
-	// give chance to the plugin to free any allocated resources
-	Free() error
-
+// Method represents an authentication method (plugin) for HTTP requests.
+type Method interface {
 	// 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
@@ -37,3 +28,33 @@ type Auth interface {
 	// 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
+}
+
+// PasswordAuthenticator represents a source of authentication
+type PasswordAuthenticator interface {
+	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) error
+}
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index c6b98c144..93806c707 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -14,11 +14,13 @@ 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.
 var (
-	_ Auth = &OAuth2{}
+	_ Method = &OAuth2{}
+	_ Named  = &OAuth2{}
 )
 
 // CheckOAuthAccessToken returns uid of user from oauth token
@@ -27,7 +29,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 +38,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() {
@@ -51,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 f958d28c9..46d8d3fa6 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -19,7 +19,8 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &ReverseProxy{}
+	_ 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 9f08f4336..9a6e2d95d 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -13,7 +13,8 @@ import (
 
 // Ensure the struct implements the interface.
 var (
-	_ Auth = &Session{}
+	_ Method = &Session{}
+	_ Named  = &Session{}
 )
 
 // Session checks if there is a user uid stored in the session and returns the user
@@ -21,21 +22,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/signin.go b/services/auth/signin.go
new file mode 100644
index 000000000..2c4bf9b35
--- /dev/null
+++ b/services/auth/signin.go
@@ -0,0 +1,113 @@
+// 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"
+
+	// 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"
+	_ "code.gitea.io/gitea/services/auth/source/smtp"
+	_ "code.gitea.io/gitea/services/auth/source/sspi"
+)
+
+// 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 {
+		source, err := models.GetLoginSourceByID(user.LoginSource)
+		if err != nil {
+			return nil, err
+		}
+
+		if !source.IsActive {
+			return nil, models.ErrLoginSourceNotActived
+		}
+
+		authenticator, ok := source.Cfg.(PasswordAuthenticator)
+		if !ok {
+			return nil, models.ErrUnsupportedLoginType
+		}
+
+		user, err := authenticator.Authenticate(user, username, password)
+		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
+	}
+
+	sources, err := models.AllActiveLoginSources()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, source := range sources {
+		if !source.IsActive {
+			// don't try to authenticate non-active sources
+			continue
+		}
+
+		authenticator, ok := source.Cfg.(PasswordAuthenticator)
+		if !ok {
+			continue
+		}
+
+		authUser, err := authenticator.Authenticate(nil, username, password)
+
+		if err == nil {
+			if !authUser.ProhibitLogin {
+				return authUser, nil
+			}
+			err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
+		}
+
+		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}
+}
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 000000000..2e0fa9ba2
--- /dev/null
+++ b/services/auth/source/db/assert_interface_test.go
@@ -0,0 +1,21 @@
+// 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"
+)
+
+// 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
+}
+
+var _ (sourceInterface) = &db.Source{}
diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go
new file mode 100644
index 000000000..e73ab15d2
--- /dev/null
+++ b/services/auth/source/db/authenticate.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 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 == nil {
+		return nil, models.ErrUserNotExist{Name: login}
+	}
+
+	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/db/source.go b/services/auth/source/db/source.go
new file mode 100644
index 000000000..182c05f0d
--- /dev/null
+++ b/services/auth/source/db/source.go
@@ -0,0 +1,31 @@
+// 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"
+
+// Source is a password authentication service
+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/modules/auth/ldap/README.md b/services/auth/source/ldap/README.md
similarity index 57%
rename from modules/auth/ldap/README.md
rename to services/auth/source/ldap/README.md
index 76841f44a..3a839fa31 100644
--- a/modules/auth/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
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 000000000..4cf3eafe7
--- /dev/null
+++ b/services/auth/source/ldap/assert_interface_test.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_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"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
+	models.SSHKeyProvider
+	models.LoginConfig
+	models.SkipVerifiable
+	models.HasTLSer
+	models.UseTLSer
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &ldap.Source{}
diff --git a/services/auth/source/ldap/security_protocol.go b/services/auth/source/ldap/security_protocol.go
new file mode 100644
index 000000000..47c9d30e5
--- /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
new file mode 100644
index 000000000..87be0117e
--- /dev/null
+++ b/services/auth/source/ldap/source.go
@@ -0,0 +1,120 @@
+// 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/secret"
+	"code.gitea.io/gitea/modules/setting"
+
+	jsoniter "github.com/json-iterator/go"
+)
+
+// .____     ________      _____ __________
+// |    |    \______ \    /  _  \\______   \
+// |    |     |    |  \  /  /_\  \|     ___/
+// |    |___  |    `   \/    |    \    |
+// |_______ \/_______  /\____|__  /____|
+//         \/        \/         \/
+
+// 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 {
+	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
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
+}
+
+// FromDB fills up a LDAPConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	err := models.JSONUnmarshalHandleDoubleEncode(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 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 > SecurityProtocolUnencrypted
+}
+
+// UseTLS returns if UseTLS
+func (source *Source) UseTLS() bool {
+	return source.SecurityProtocol != SecurityProtocolUnencrypted
+}
+
+// ProvidesSSHKeys returns if this source provides SSH Keys
+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
new file mode 100644
index 000000000..1d5e69539
--- /dev/null
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -0,0 +1,93 @@
+// 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"
+)
+
+// 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) (*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}
+	}
+
+	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.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.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.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.SynchronizePublicKeys(user, source.loginSource, 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:     composeFullName(sr.Name, sr.Surname, sr.Username),
+		Email:        sr.Mail,
+		LoginType:    source.loginSource.Type,
+		LoginSource:  source.loginSource.ID,
+		LoginName:    login,
+		IsActive:     true,
+		IsAdmin:      sr.IsAdmin,
+		IsRestricted: sr.IsRestricted,
+	}
+
+	err := models.CreateUser(user)
+
+	if err == nil && isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
+		err = models.RewriteAllPublicKeys()
+	}
+
+	return user, err
+}
diff --git a/modules/auth/ldap/ldap.go b/services/auth/source/ldap/source_search.go
similarity index 85%
rename from modules/auth/ldap/ldap.go
rename to services/auth/source/ldap/source_search.go
index 91ad33a60..e99fc6790 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,47 +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
-)
-
-// 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
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
new file mode 100644
index 000000000..7e4088e57
--- /dev/null
+++ b/services/auth/source/ldap/source_sync.go
@@ -0,0 +1,184 @@
+// 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"
+)
+
+// Sync causes this ldap source to synchronize its users with the db
+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(source.loginSource)
+	if err != nil {
+		log.Error("SyncExternalUsers: %v", err)
+		return err
+	}
+	select {
+	case <-ctx.Done():
+		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", source.loginSource.Name)
+		return nil
+	}
+
+	if len(sr) == 0 {
+		if !source.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", 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()
+				if err != nil {
+					log.Error("RewriteAllPublicKeys: %v", err)
+				}
+			}
+			return models.ErrCancelledf("During update of %s before completed update of users", source.loginSource.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", source.loginSource.Name, su.Username)
+
+			usr = &models.User{
+				LowerName:    strings.ToLower(su.Username),
+				Name:         su.Username,
+				FullName:     fullName,
+				LoginType:    source.loginSource.Type,
+				LoginSource:  source.loginSource.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", source.loginSource.Name, su.Username, err)
+			} else if isAttributeSSHPublicKeySet {
+				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
+				}
+			}
+		} else if updateExisting {
+			existingUsers = append(existingUsers, usr.ID)
+
+			// Synchronize SSH Public Key if that attribute is set
+			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
+				sshKeysNeedUpdate = true
+			}
+
+			// Check if user data has changed
+			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 {
+
+				log.Trace("SyncExternalUsers[%s]: Updating user %s", source.loginSource.Name, usr.Name)
+
+				usr.FullName = fullName
+				usr.Email = su.Mail
+				// Change existing admin flag only if AdminFilter option is set
+				if len(source.AdminFilter) > 0 {
+					usr.IsAdmin = su.IsAdmin
+				}
+				// Change existing restricted flag only if RestrictedFilter option is set
+				if !usr.IsAdmin && len(source.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", source.loginSource.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", source.loginSource.Name)
+		return models.ErrCancelledf("During update of %s before delete users", source.loginSource.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", 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", source.loginSource.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 000000000..f27de37c8
--- /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/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
new file mode 100644
index 000000000..4157427ff
--- /dev/null
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -0,0 +1,23 @@
+// 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"
+	"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
+	models.RegisterableSource
+	auth.PasswordAuthenticator
+}
+
+var _ (sourceInterface) = &oauth2.Source{}
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
new file mode 100644
index 000000000..f797fd7fd
--- /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()
+		if err != nil {
+			log.Critical("Unable to register source: %s due to Error: %v. This source will be disabled.", source.Name, err)
+			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
+			}
+		}
+	}
+	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/modules/auth/oauth2/oauth2.go b/services/auth/source/oauth2/providers.go
similarity index 61%
rename from modules/auth/oauth2/oauth2.go
rename to services/auth/source/oauth2/providers.go
index 5d152e0a5..bf97f8002 100644
--- a/modules/auth/oauth2/oauth2.go
+++ b/services/auth/source/oauth2/providers.go
@@ -1,20 +1,18 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
+// 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"
 	"net/url"
+	"sort"
 
+	"code.gitea.io/gitea/models"
 	"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"
@@ -28,79 +26,94 @@ import (
 	"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
+// Provider describes the display values of a single OAuth2 provider
+type Provider struct {
+	Name             string
+	DisplayName      string
+	Image            string
+	CustomURLMapping *CustomURLMapping
 }
 
-// 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))
+// 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]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()
 	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()
+		return nil, nil, err
 	}
 
-	gothic.GetProviderName = func(req *http.Request) (string, error) {
-		return req.Header.Get(providerHeaderKey), nil
+	var orderedKeys []string
+	providers := make(map[string]Provider)
+	for _, source := range loginSources {
+		prov := Providers[source.Cfg.(*Source).Provider]
+		if source.Cfg.(*Source).IconURL != "" {
+			prov.Image = source.Cfg.(*Source).IconURL
+		}
+		providers[source.Name] = prov
+		orderedKeys = append(orderedKeys, source.Name)
 	}
 
-	return nil
-}
+	sort.Strings(orderedKeys)
 
-// 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
+	return orderedKeys, providers, nil
 }
 
 // RegisterProvider register a OAuth2 provider in goth lib
@@ -242,58 +255,3 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
 
 	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/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
new file mode 100644
index 000000000..e9c49ef90
--- /dev/null
+++ b/services/auth/source/oauth2/source.go
@@ -0,0 +1,51 @@
+// 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"
+
+	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              *CustomURLMapping
+	IconURL                       string
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
+}
+
+// FromDB fills up an OAuth2Config from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
+}
+
+// ToDB exports an SMTPConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	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
new file mode 100644
index 000000000..2e39f245d
--- /dev/null
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -0,0 +1,15 @@
+// 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/services/auth/source/db"
+)
+
+// 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 000000000..8f4663f3b
--- /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
+}
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
new file mode 100644
index 000000000..b61cc3fe7
--- /dev/null
+++ b/services/auth/source/oauth2/source_register.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 oauth2
+
+import (
+	"code.gitea.io/gitea/models"
+)
+
+// RegisterSource causes an OAuth2 configuration to be registered
+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() error {
+	RemoveProvider(source.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, source *Source) error {
+	if err != nil && source.Provider == "openidConnect" {
+		err = models.ErrOpenIDConnectInitialize{ProviderName: providerName, OpenIDConnectAutoDiscoveryURL: source.OpenIDConnectAutoDiscoveryURL, Cause: err}
+	}
+	return err
+}
diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go
new file mode 100644
index 000000000..0573a47e3
--- /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 signed 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 000000000..68829fba2
--- /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/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go
new file mode 100644
index 000000000..a0bebdf9c
--- /dev/null
+++ b/services/auth/source/pam/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 pam_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/services/auth"
+	"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
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &pam.Source{}
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
new file mode 100644
index 000000000..b717ee6fe
--- /dev/null
+++ b/services/auth/source/pam/source.go
@@ -0,0 +1,47 @@
+// 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
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
+}
+
+// FromDB fills up a PAMConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	return models.JSONUnmarshalHandleDoubleEncode(bs, &source)
+}
+
+// ToDB exports a PAMConfig to a serialized format.
+func (source *Source) ToDB() ([]byte, error) {
+	json := jsoniter.ConfigCompatibleWithStandardLibrary
+	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
new file mode 100644
index 000000000..6ca064290
--- /dev/null
+++ b/services/auth/source/pam/source_authenticate.go
@@ -0,0 +1,62 @@
+// 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"
+)
+
+// 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) {
+	pamLogin, err := pam.Auth(source.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 source.EmailDomain != "" {
+			email = fmt.Sprintf("%s@%s", username, source.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: source.loginSource.ID,
+		LoginName:   login, // This is what the user typed in
+		IsActive:    true,
+	}
+	return user, models.CreateUser(user)
+}
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 000000000..bc2042e06
--- /dev/null
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -0,0 +1,25 @@
+// 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"
+)
+
+// 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
+	models.SkipVerifiable
+	models.HasTLSer
+	models.UseTLSer
+	models.LoginSourceSettable
+}
+
+var _ (sourceInterface) = &smtp.Source{}
diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go
new file mode 100644
index 000000000..8edf4fca1
--- /dev/null
+++ b/services/auth/source/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, source *Source) error {
+	c, err := smtp.Dial(fmt.Sprintf("%s:%d", source.Host, source.Port))
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+
+	if err = c.Hello("gogs"); err != nil {
+		return err
+	}
+
+	if source.TLS {
+		if ok, _ := c.Extension("STARTTLS"); ok {
+			if err = c.StartTLS(&tls.Config{
+				InsecureSkipVerify: source.SkipVerify,
+				ServerName:         source.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/source/smtp/source.go b/services/auth/source/smtp/source.go
new file mode 100644
index 000000000..0f948d538
--- /dev/null
+++ b/services/auth/source/smtp/source.go
@@ -0,0 +1,66 @@
+// 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
+
+	// reference to the loginSource
+	loginSource *models.LoginSource
+}
+
+// FromDB fills up an SMTPConfig from serialized format.
+func (source *Source) FromDB(bs []byte) error {
+	return models.JSONUnmarshalHandleDoubleEncode(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
+}
+
+// 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
new file mode 100644
index 000000000..9bab86604
--- /dev/null
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -0,0 +1,71 @@
+// 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"
+)
+
+// 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) (*models.User, error) {
+	// Verify allowed domains.
+	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(source.AllowedDomains, ","), true) {
+			return nil, models.ErrUserNotExist{Name: login}
+		}
+	}
+
+	var auth smtp.Auth
+	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, source); err != nil {
+		// Check standard error format first,
+		// then fallback to worse case.
+		tperr, ok := err.(*textproto.Error)
+		if (ok && tperr.Code == 535) ||
+			strings.Contains(err.Error(), "Username and Password not accepted") {
+			return nil, 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: source.loginSource.ID,
+		LoginName:   login,
+		IsActive:    true,
+	}
+	return user, models.CreateUser(user)
+}
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 000000000..605a6ec6c
--- /dev/null
+++ b/services/auth/source/sspi/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 sspi_test
+
+import (
+	"code.gitea.io/gitea/models"
+	"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
+}
+
+var _ (sourceInterface) = &sspi.Source{}
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
new file mode 100644
index 000000000..e4be446f3
--- /dev/null
+++ b/services/auth/source/sspi/source.go
@@ -0,0 +1,41 @@
+// 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 {
+	return models.JSONUnmarshalHandleDoubleEncode(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 bb0291d2c..8420d4307 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"
@@ -32,7 +33,10 @@ var (
 	sspiAuth *websspi.Authenticator
 
 	// Ensure the struct implements the interface.
-	_ Auth = &SSPI{}
+	_ Method        = &SSPI{}
+	_ Named         = &SSPI{}
+	_ Initializable = &SSPI{}
+	_ Freeable      = &SSPI{}
 )
 
 // SSPI implements the SingleSignOn interface and authenticates requests
@@ -146,7 +150,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 +161,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 +181,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 +218,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 +226,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
new file mode 100644
index 000000000..a34b4d1d2
--- /dev/null
+++ b/services/auth/sync.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 auth
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// 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.IsActive || !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 syncable, ok := s.Cfg.(SynchronizableSource); ok {
+			err := syncable.Sync(ctx, updateExisting)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index d825cd7d1..3fbfedefe 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>
@@ -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 d5d8aadb5..35ab97602 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 9c2436dd3..5aa928208 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}}