Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187)

Fix #880

Design:

1. A global setting `security.TWO_FACTOR_AUTH`.
* To support org-level config, we need to introduce a better "owner
setting" system first (in the future)
2. A user without 2FA can login and may explore, but can NOT read or
write to any repositories via API/web.
3. Keep things as simple as possible.
* This option only aggressively suggest users to enable their 2FA at the
moment, it does NOT guarantee that users must have 2FA before all other
operations, it should be good enough for real world use cases.
* Some details and tests could be improved in the future since this
change only adds a check and seems won't affect too much.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
wxiaoguang 2025-04-29 06:31:59 +08:00 committed by GitHub
parent 4ed07244b9
commit 0148d03f21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 324 additions and 223 deletions

View File

@ -9,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap"
"github.com/urfave/cli/v2"
@ -210,8 +211,8 @@ func newAuthService() *authService {
}
}
// parseAuthSource assigns values on authSource according to command line flags.
func parseAuthSource(c *cli.Context, authSource *auth.Source) {
// parseAuthSourceLdap assigns values on authSource according to command line flags.
func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
if c.IsSet("name") {
authSource.Name = c.String("name")
}
@ -227,6 +228,7 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) {
if c.IsSet("disable-synchronize-users") {
authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
}
authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
}
// parseLdapConfig assigns values on config according to command line flags.
@ -298,9 +300,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("allow-deactivate-all") {
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
}
if c.IsSet("skip-local-2fa") {
config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
}
if c.IsSet("enable-groups") {
config.GroupsEnabled = c.Bool("enable-groups")
}
@ -376,7 +375,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
},
}
parseAuthSource(c, authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@ -398,7 +397,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
return err
}
parseAuthSource(c, authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@ -427,7 +426,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
},
}
parseAuthSource(c, authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
@ -449,7 +448,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
return err
}
parseAuthSource(c, authSource)
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}

View File

@ -9,6 +9,7 @@ import (
"net/url"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/urfave/cli/v2"
@ -156,7 +157,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
CustomURLMapping: customURLMapping,
IconURL: c.String("icon-url"),
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"),
@ -185,10 +185,11 @@ func runAddOauth(c *cli.Context) error {
}
return auth_model.CreateSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: config,
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: config,
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
})
}
@ -294,6 +295,6 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source)
}

View File

@ -117,9 +117,6 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
if c.IsSet("disable-helo") {
conf.DisableHelo = c.Bool("disable-helo")
}
if c.IsSet("skip-local-2fa") {
conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
}
return nil
}
@ -156,10 +153,11 @@ func runAddSMTP(c *cli.Context) error {
}
return auth_model.CreateSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
Cfg: &smtpConfig,
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
Cfg: &smtpConfig,
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
})
}
@ -195,6 +193,6 @@ func runUpdateSMTP(c *cli.Context) error {
}
source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source)
}

View File

@ -524,6 +524,10 @@ INTERNAL_TOKEN =
;;
;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
;; RECORD_USER_SIGNUP_METADATA = false
;;
;; Set the two-factor auth behavior.
;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web.
;TWO_FACTOR_AUTH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -58,6 +58,15 @@ var Names = map[Type]string{
// Config represents login config as far as the db is concerned
type Config interface {
convert.Conversion
SetAuthSource(*Source)
}
type ConfigBase struct {
AuthSource *Source
}
func (p *ConfigBase) SetAuthSource(s *Source) {
p.AuthSource = s
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
@ -104,19 +113,15 @@ func RegisterTypeConfig(typ Type, exemplar Config) {
}
}
// SourceSettable configurations can have their authSource set on them
type SourceSettable interface {
SetAuthSource(*Source)
}
// Source represents an external way for authorizing users.
type Source struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg convert.Conversion `xorm:"TEXT"`
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
Cfg Config `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) {
return
}
source.Cfg = constructor()
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
}
}
@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool {
return ok && skipVerifiable.IsSkipVerify()
}
func (source *Source) TwoFactorShouldSkip() bool {
return source.TwoFactorPolicy == "skip"
}
// CreateSource inserts a AuthSource in the DB if not already
// existing with the given name.
func CreateSource(ctx context.Context, source *Source) error {
@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {

View File

@ -19,6 +19,8 @@ import (
)
type TestSource struct {
auth_model.ConfigBase
Provider string
ClientID string
ClientSecret string

View File

@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
}
return nil
}
func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) {
has, err := HasTwoFactorByUID(ctx, id)
if err != nil {
return false, err
} else if has {
return true, nil
}
return HasWebAuthnRegistrationsByUID(ctx, id)
}

View File

@ -381,6 +381,7 @@ func prepareMigrationTasks() []*migration {
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
}
return preparedMigrations
}

View File

@ -0,0 +1,57 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_24 //nolint
import (
"code.gitea.io/gitea/modules/json"
"xorm.io/xorm"
)
func MigrateSkipTwoFactor(x *xorm.Engine) error {
type LoginSource struct {
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
}
_, err := x.SyncWithOptions(
xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
},
new(LoginSource),
)
if err != nil {
return err
}
type LoginSourceSimple struct {
ID int64
Cfg string
}
var loginSources []LoginSourceSimple
err = x.Table("login_source").Find(&loginSources)
if err != nil {
return err
}
for _, source := range loginSources {
if source.Cfg == "" {
continue
}
var cfg map[string]any
err = json.Unmarshal([]byte(source.Cfg), &cfg)
if err != nil {
return err
}
if cfg["SkipLocalTwoFA"] == true {
_, err = x.Exec("UPDATE login_source SET two_factor_policy = 'skip' WHERE id = ?", source.ID)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -522,3 +522,7 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u
return perm.CanRead(unitType)
}
func PermissionNoAccess() Permission {
return Permission{AccessMode: perm_model.AccessModeNone}
}

11
modules/session/key.go Normal file
View File

@ -0,0 +1,11 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package session
const (
KeyUID = "uid"
KeyUname = "uname"
KeyUserHasTwoFactorAuth = "userHasTwoFactorAuth"
)

View File

@ -39,6 +39,7 @@ var (
CSRFCookieName = "_csrf"
CSRFCookieHTTPOnly = true
RecordUserSignupMetadata = false
TwoFactorAuthEnforced = false
)
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
@ -142,6 +143,15 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String()
switch twoFactorAuth {
case "":
case "enforced":
TwoFactorAuthEnforced = true
default:
log.Fatal("Invalid two-factor auth option: %s", twoFactorAuth)
}
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
if InstallLock && InternalToken == "" {
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate

View File

@ -450,6 +450,7 @@ use_scratch_code = Use a scratch code
twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code.
twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your device, use your scratch code to sign in.
twofa_scratch_token_incorrect = Your scratch code is incorrect.
twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again.
login_userpass = Sign In
login_openid = OpenID
oauth_signup_tab = Register New Account

View File

@ -64,6 +64,7 @@
package v1
import (
gocontext "context"
"errors"
"fmt"
"net/http"
@ -211,11 +212,20 @@ func repoAssignment() func(ctx *context.APIContext) {
}
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if needTwoFactor {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
if !ctx.Repo.Permission.HasAnyUnitAccess() {
@ -225,6 +235,20 @@ func repoAssignment() func(ctx *context.APIContext) {
}
}
func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool, error) {
if !setting.TwoFactorAuthEnforced {
return false, nil
}
if doer == nil {
return false, nil
}
has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, doer.ID)
if err != nil {
return false, err
}
return !has, nil
}
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {

View File

@ -28,8 +28,6 @@ import (
"code.gitea.io/gitea/services/auth/source/sspi"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"xorm.io/xorm/convert"
)
const (
@ -149,7 +147,6 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
@ -163,7 +160,6 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
SkipVerify: form.SkipVerify,
HeloHostname: form.HeloHostname,
DisableHelo: form.DisableHelo,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
@ -198,7 +194,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
Scopes: scopes,
RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocalTwoFA,
GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup,
@ -252,7 +247,7 @@ func NewAuthSourcePost(ctx *context.Context) {
ctx.Data["SSPIDefaultLanguage"] = ""
hasTLS := false
var config convert.Conversion
var config auth.Config
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
@ -262,9 +257,8 @@ func NewAuthSourcePost(ctx *context.Context) {
hasTLS = true
case auth.PAM:
config = &pam_service.Source{
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
SkipLocalTwoFA: form.SkipLocalTwoFA,
ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
@ -302,11 +296,12 @@ func NewAuthSourcePost(ctx *context.Context) {
}
if err := auth.CreateSource(ctx, &auth.Source{
Type: auth.Type(form.Type),
Name: form.Name,
IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
Cfg: config,
Type: auth.Type(form.Type),
Name: form.Name,
IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled,
TwoFactorPolicy: form.TwoFactorPolicy,
Cfg: config,
}); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
@ -384,7 +379,7 @@ func EditAuthSourcePost(ctx *context.Context) {
return
}
var config convert.Conversion
var config auth.Config
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
@ -421,6 +416,7 @@ func EditAuthSourcePost(ctx *context.Context) {
source.IsActive = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
source.TwoFactorPolicy = form.TwoFactorPolicy
if err := auth.UpdateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@ -87,6 +88,7 @@ func TwoFactorPost(ctx *context.Context) {
return
}
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
handleSignIn(ctx, u, remember)
return
}

View File

@ -76,6 +76,10 @@ func autoSignIn(ctx *context.Context) (bool, error) {
}
return false, nil
}
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
}
isSucceed = true
@ -87,9 +91,9 @@ func autoSignIn(ctx *context.Context) (bool, error) {
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
if err := updateSession(ctx, nil, map[string]any{
// Set session IDs
"uid": u.ID,
"uname": u.Name,
session.KeyUID: u.ID,
session.KeyUname: u.Name,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
return false, fmt.Errorf("unable to updateSession: %w", err)
}
@ -239,9 +243,8 @@ func SignInPost(ctx *context.Context) {
}
// Now handle 2FA:
// First of all if the source can skip local two fa we're done
if skipper, ok := source.Cfg.(auth_service.LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
if source.TwoFactorShouldSkip() {
handleSignIn(ctx, u, form.Remember)
return
}
@ -262,7 +265,7 @@ func SignInPost(ctx *context.Context) {
}
if !hasTOTPtwofa && !hasWebAuthnTwofa {
// No two factor auth configured we can sign in the user
// No two-factor auth configured we can sign in the user
handleSignIn(ctx, u, form.Remember)
return
}
@ -311,8 +314,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
}
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
return setting.AppSubURL + "/"
}
if err := updateSession(ctx, []string{
// Delete the openid, 2fa and linkaccount data
// Delete the openid, 2fa and link_account data
"openid_verified_uri",
"openid_signin_remember",
"openid_determined_email",
@ -321,8 +330,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
"twofaRemember",
"linkAccount",
}, map[string]any{
"uid": u.ID,
"uname": u.Name,
session.KeyUID: u.ID,
session.KeyUname: u.Name,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/"

View File

@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
@ -302,7 +303,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
needs2FA := false
if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
if !source.TwoFactorShouldSkip() {
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err)
@ -352,10 +353,16 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
ctx.ServerError("UpdateUser", err)
return
}
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := updateSession(ctx, nil, map[string]any{
"uid": u.ID,
"uname": u.Name,
session.KeyUID: u.ID,
session.KeyUname: u.Name,
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
ctx.ServerError("updateSession", err)
return

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
@ -163,6 +164,7 @@ func EnrollTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
@ -194,6 +196,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
@ -246,6 +249,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
newTwoFactorErr := auth.NewTwoFactor(ctx, t)
if newTwoFactorErr == nil {
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
}
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil {
@ -261,10 +268,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
log.Error("Unable to save changes to the session: %v", err)
}
if err = auth.NewTwoFactor(ctx, t); err != nil {
if newTwoFactorErr != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
return
}

View File

@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
@ -120,7 +121,7 @@ func WebauthnRegisterPost(ctx *context.Context) {
return
}
_ = ctx.Session.Delete("webauthnName")
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
ctx.JSON(http.StatusCreated, cred)
}

View File

@ -142,14 +142,14 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
// Check if the user has webAuthn registration
if !source.TwoFactorShouldSkip() {
// Check if the user has WebAuthn registration
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
if err != nil {
return nil, err
}
if hasWebAuthn {
return nil, errors.New("Basic authorization is not allowed while webAuthn enrolled")
return nil, errors.New("basic authorization is not allowed while WebAuthn enrolled")
}
if err := validateTOTP(req, u); err != nil {

View File

@ -35,11 +35,6 @@ type PasswordAuthenticator interface {
Authenticate(ctx context.Context, user *user_model.User, login, password string) (*user_model.User, error)
}
// LocalTwoFASkipper represents a source of authentication that can skip local 2fa
type LocalTwoFASkipper interface {
IsSkipLocalTwoFA() bool
}
// SynchronizableSource represents a source that can synchronize users
type SynchronizableSource interface {
Sync(ctx context.Context, updateExisting bool) error

View File

@ -11,7 +11,9 @@ import (
)
// Source is a password authentication service
type Source struct{}
type Source struct {
auth.ConfigBase `json:"-"`
}
// FromDB fills up an OAuth2Config from serialized format.
func (source *Source) FromDB(bs []byte) error {

View File

@ -15,13 +15,11 @@ import (
type sourceInterface interface {
auth.PasswordAuthenticator
auth.SynchronizableSource
auth.LocalTwoFASkipper
auth_model.SSHKeyProvider
auth_model.Config
auth_model.SkipVerifiable
auth_model.HasTLSer
auth_model.UseTLSer
auth_model.SourceSettable
}
var _ (sourceInterface) = &ldap.Source{}

View File

@ -24,6 +24,8 @@ import (
// Source Basic LDAP authentication service
type Source struct {
auth.ConfigBase `json:"-"`
Name string // canonical name (ie. corporate.ad)
Host string // LDAP host
Port int // port number
@ -54,9 +56,6 @@ type Source struct {
GroupTeamMap string // Map LDAP groups to teams
GroupTeamMapRemoval bool // Remove user from teams which are synchronized and user is not a member of the corresponding LDAP group
UserUID string // User Attribute listed in Group
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
authSource *auth.Source // reference to the authSource
}
// FromDB fills up a LDAPConfig from serialized format.
@ -109,11 +108,6 @@ func (source *Source) ProvidesSSHKeys() bool {
return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.LDAP, &Source{})
auth.RegisterTypeConfig(auth.DLDAP, &Source{})

View File

@ -25,7 +25,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
if user != nil {
loginName = user.LoginName
}
sr := source.SearchEntry(loginName, password, source.authSource.Type == auth.DLDAP)
sr := source.SearchEntry(loginName, password, source.AuthSource.Type == auth.DLDAP)
if sr == nil {
// User not in LDAP, do nothing
return nil, user_model.ErrUserNotExist{Name: loginName}
@ -73,7 +73,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
if user != nil {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey) {
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
@ -84,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginType: source.AuthSource.Type,
LoginSource: source.AuthSource.ID,
LoginName: userName,
IsAdmin: sr.IsAdmin,
}
@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, err
}
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey) {
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
@ -123,8 +123,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil
}
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
func (source *Source) IsSkipLocalTwoFA() bool {
return source.SkipLocalTwoFA
}

View File

@ -22,21 +22,21 @@ import (
// 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.authSource.Name)
log.Trace("Doing: SyncExternalUsers[%s]", source.AuthSource.Name)
isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
var sshKeysNeedUpdate bool
// Find all users with this login type - FIXME: Should this be an iterator?
users, err := user_model.GetUsersBySource(ctx, source.authSource)
users, err := user_model.GetUsersBySource(ctx, source.AuthSource)
if err != nil {
log.Error("SyncExternalUsers: %v", err)
return err
}
select {
case <-ctx.Done():
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.authSource.Name)
return db.ErrCancelledf("Before update of %s", source.authSource.Name)
log.Warn("SyncExternalUsers: Cancelled before update of %s", source.AuthSource.Name)
return db.ErrCancelledf("Before update of %s", source.AuthSource.Name)
default:
}
@ -51,7 +51,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
sr, err := source.SearchEntries()
if err != nil {
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.authSource.Name)
log.Error("SyncExternalUsers LDAP source failure [%s], skipped", source.AuthSource.Name)
return nil
}
@ -74,7 +74,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
for _, su := range sr {
select {
case <-ctx.Done():
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.AuthSource.Name)
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
if sshKeysNeedUpdate {
err = asymkey_service.RewriteAllPublicKeys(ctx)
@ -82,7 +82,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Error("RewriteAllPublicKeys: %v", err)
}
}
return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name)
return db.ErrCancelledf("During update of %s before completed update of users", source.AuthSource.Name)
default:
}
if su.Username == "" && su.Mail == "" {
@ -111,14 +111,14 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
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.authSource.Name, su.Username)
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.AuthSource.Name, su.Username)
usr = &user_model.User{
LowerName: su.LowerName,
Name: su.Username,
FullName: fullName,
LoginType: source.authSource.Type,
LoginSource: source.authSource.ID,
LoginType: source.AuthSource.Type,
LoginSource: source.AuthSource.ID,
LoginName: su.Username,
Email: su.Mail,
IsAdmin: su.IsAdmin,
@ -130,12 +130,12 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
if err != nil {
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.AuthSource.Name, su.Username, err)
}
if err == nil && isAttributeSSHPublicKeySet {
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.authSource.Name, usr.Name)
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.authSource, su.SSHPublicKey) {
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.AuthSource.Name, usr.Name)
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
sshKeysNeedUpdate = true
}
}
@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
} else if updateExisting {
// Synchronize SSH Public Key if that attribute is set
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.authSource, su.SSHPublicKey) {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey) {
sshKeysNeedUpdate = true
}
@ -155,7 +155,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
!strings.EqualFold(usr.Email, su.Mail) ||
usr.FullName != fullName ||
!usr.IsActive {
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.authSource.Name, usr.Name)
log.Trace("SyncExternalUsers[%s]: Updating user %s", source.AuthSource.Name, usr.Name)
opts := &user_service.UpdateOptions{
FullName: optional.Some(fullName),
@ -170,11 +170,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err)
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.AuthSource.Name, usr.Name, err)
}
if err := user_service.ReplacePrimaryEmailAddress(ctx, usr, su.Mail); err != nil {
log.Error("SyncExternalUsers[%s]: Error updating user %s primary email %s: %v", source.authSource.Name, usr.Name, su.Mail, err)
log.Error("SyncExternalUsers[%s]: Error updating user %s primary email %s: %v", source.AuthSource.Name, usr.Name, su.Mail, err)
}
}
@ -202,8 +202,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
select {
case <-ctx.Done():
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.authSource.Name)
return db.ErrCancelledf("During update of %s before delete users", source.authSource.Name)
log.Warn("SyncExternalUsers: Cancelled during update of %s before delete users", source.AuthSource.Name)
return db.ErrCancelledf("During update of %s before delete users", source.AuthSource.Name)
default:
}
@ -214,13 +214,13 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
continue
}
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.authSource.Name, usr.Name)
log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.AuthSource.Name, usr.Name)
opts := &user_service.UpdateOptions{
IsActive: optional.Some(false),
}
if err := user_service.UpdateUser(ctx, usr, opts); err != nil {
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.authSource.Name, usr.Name, err)
log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.AuthSource.Name, usr.Name, err)
}
}
}

View File

@ -14,7 +14,6 @@ import (
type sourceInterface interface {
auth_model.Config
auth_model.SourceSettable
auth_model.RegisterableSource
auth.PasswordAuthenticator
}

View File

@ -10,6 +10,8 @@ import (
// Source holds configuration for the OAuth2 login source.
type Source struct {
auth.ConfigBase `json:"-"`
Provider string
ClientID string
ClientSecret string
@ -25,10 +27,6 @@ type Source struct {
GroupTeamMap string
GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
}
// FromDB fills up an OAuth2Config from serialized format.
@ -41,11 +39,6 @@ func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.OAuth2, &Source{})
}

View File

@ -13,7 +13,7 @@ import (
// 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.authSource.Name)
request.Header.Set(ProviderHeaderKey, source.AuthSource.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
@ -33,7 +33,7 @@ func (source *Source) Callout(request *http.Request, response http.ResponseWrite
// 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.authSource.Name)
request.Header.Set(ProviderHeaderKey, source.AuthSource.Name)
gothRWMutex.RLock()
defer gothRWMutex.RUnlock()

View File

@ -9,13 +9,13 @@ import (
// RegisterSource causes an OAuth2 configuration to be registered
func (source *Source) RegisterSource() error {
err := RegisterProviderWithGothic(source.authSource.Name, source)
return wrapOpenIDConnectInitializeError(err, source.authSource.Name, source)
err := RegisterProviderWithGothic(source.AuthSource.Name, source)
return wrapOpenIDConnectInitializeError(err, source.AuthSource.Name, source)
}
// UnregisterSource causes an OAuth2 configuration to be unregistered
func (source *Source) UnregisterSource() error {
RemoveProviderFromGothic(source.authSource.Name)
RemoveProviderFromGothic(source.AuthSource.Name)
return nil
}

View File

@ -18,27 +18,27 @@ import (
// Sync causes this OAuth2 source to synchronize its users with the db.
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID)
log.Trace("Doing: SyncExternalUsers[%s] %d", source.AuthSource.Name, source.AuthSource.ID)
if !updateExisting {
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name)
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.AuthSource.Name)
return nil
}
provider, err := createProvider(source.authSource.Name, source)
provider, err := createProvider(source.AuthSource.Name, source)
if err != nil {
return err
}
if !provider.RefreshTokenAvailable() {
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name)
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.AuthSource.Name)
return nil
}
opts := user_model.FindExternalUserOptions{
HasRefreshToken: true,
Expired: true,
LoginSourceID: source.authSource.ID,
LoginSourceID: source.AuthSource.ID,
}
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
@ -77,7 +77,7 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us
// recognizes them as a valid user, they will be able to login
// via their provider and reactivate their account.
if shouldDisable {
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID)
log.Info("SyncExternalUsers[%s] disabling user %d", source.AuthSource.Name, user.ID)
return db.WithTx(ctx, func(ctx context.Context) error {
if hasUser {

View File

@ -18,19 +18,21 @@ func TestSource(t *testing.T) {
source := &Source{
Provider: "fake",
authSource: &auth.Source{
ID: 12,
Type: auth.OAuth2,
Name: "fake",
IsActive: true,
IsSyncEnabled: true,
ConfigBase: auth.ConfigBase{
AuthSource: &auth.Source{
ID: 12,
Type: auth.OAuth2,
Name: "fake",
IsActive: true,
IsSyncEnabled: true,
},
},
}
user := &user_model.User{
LoginName: "external",
LoginType: auth.OAuth2,
LoginSource: source.authSource.ID,
LoginSource: source.AuthSource.ID,
Name: "test",
Email: "external@example.com",
}
@ -47,7 +49,7 @@ func TestSource(t *testing.T) {
err = user_model.LinkExternalToUser(t.Context(), user, e)
assert.NoError(t, err)
provider, err := createProvider(source.authSource.Name, source)
provider, err := createProvider(source.AuthSource.Name, source)
assert.NoError(t, err)
t.Run("refresh", func(t *testing.T) {

View File

@ -15,7 +15,6 @@ import (
type sourceInterface interface {
auth.PasswordAuthenticator
auth_model.Config
auth_model.SourceSettable
}
var _ (sourceInterface) = &pam.Source{}

View File

@ -17,12 +17,10 @@ import (
// Source holds configuration for the PAM login source.
type Source struct {
ServiceName string // pam service (e.g. system-auth)
EmailDomain string
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
auth.ConfigBase `json:"-"`
// reference to the authSource
authSource *auth.Source
ServiceName string // pam service (e.g. system-auth)
EmailDomain string
}
// FromDB fills up a PAMConfig from serialized format.
@ -35,11 +33,6 @@ func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.PAM, &Source{})
}

View File

@ -56,7 +56,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
Email: email,
Passwd: password,
LoginType: auth.PAM,
LoginSource: source.authSource.ID,
LoginSource: source.AuthSource.ID,
LoginName: userName, // This is what the user typed in
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
@ -69,8 +69,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil
}
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
func (source *Source) IsSkipLocalTwoFA() bool {
return source.SkipLocalTwoFA
}

View File

@ -18,7 +18,6 @@ type sourceInterface interface {
auth_model.SkipVerifiable
auth_model.HasTLSer
auth_model.UseTLSer
auth_model.SourceSettable
}
var _ (sourceInterface) = &smtp.Source{}

View File

@ -17,6 +17,8 @@ import (
// Source holds configuration for the SMTP login source.
type Source struct {
auth.ConfigBase `json:"-"`
Auth string
Host string
Port int
@ -25,10 +27,6 @@ type Source struct {
SkipVerify bool
HeloHostname string
DisableHelo bool
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
}
// FromDB fills up an SMTPConfig from serialized format.
@ -56,11 +54,6 @@ func (source *Source) UseTLS() bool {
return source.ForceSMTPS || source.Port == 465
}
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() {
auth.RegisterTypeConfig(auth.SMTP, &Source{})
}

View File

@ -72,7 +72,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
Email: userName,
Passwd: password,
LoginType: auth_model.SMTP,
LoginSource: source.authSource.ID,
LoginSource: source.AuthSource.ID,
LoginName: userName,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
@ -85,8 +85,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil
}
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
func (source *Source) IsSkipLocalTwoFA() bool {
return source.SkipLocalTwoFA
}

View File

@ -17,6 +17,8 @@ import (
// Source holds configuration for SSPI single sign-on.
type Source struct {
auth.ConfigBase `json:"-"`
AutoCreateUsers bool
AutoActivateUsers bool
StripDomainNames bool

View File

@ -196,6 +196,8 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["SystemConfig"] = setting.Config()
ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth()
// FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these
ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations
ctx.Data["DisableStars"] = setting.Repository.DisableStars
@ -209,6 +211,13 @@ func Contexter() func(next http.Handler) http.Handler {
}
}
func (ctx *Context) DoerNeedTwoFactorAuth() bool {
if !setting.TwoFactorAuthEnforced {
return false
}
return ctx.Session.Get(session.KeyUserHasTwoFactorAuth) == false
}
// HasError returns true if error occurs in form validation.
// Attention: this function changes ctx.Data and ctx.Flash
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.

View File

@ -340,10 +340,14 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
return
}
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
if ctx.DoerNeedTwoFactorAuth() {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
}
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {

View File

@ -14,9 +14,11 @@ import (
// AuthenticationForm form for authentication
type AuthenticationForm struct {
ID int64
Type int `binding:"Range(2,7)"`
Name string `binding:"Required;MaxSize(30)"`
ID int64
Type int `binding:"Range(2,7)"`
Name string `binding:"Required;MaxSize(30)"`
TwoFactorPolicy string
Host string
Port int
BindDN string
@ -74,7 +76,6 @@ type AuthenticationForm struct {
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool

View File

@ -17,6 +17,13 @@
<label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label>
<input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required>
</div>
<div class="inline field">
<div class="ui checkbox">
<label ><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input name="two_factor_policy" type="checkbox" value="skip" {{if eq .Source.TwoFactorPolicy "skip"}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
<!-- LDAP and DLDAP -->
{{if or .Source.IsLDAP .Source.IsDLDAP}}
@ -159,13 +166,6 @@
</div>
</div>
{{end}}
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="allow_deactivate_all"><strong>{{ctx.Locale.Tr "admin.auths.allow_deactivate_all"}}</strong></label>
@ -227,13 +227,6 @@
<input id="allowed_domains" name="allowed_domains" value="{{$cfg.AllowedDomains}}">
<p class="help">{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}</p>
</div>
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
{{end}}
<!-- PAM -->
@ -247,13 +240,6 @@
<label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label>
<input id="pam_email_domain" name="pam_email_domain" value="{{$cfg.EmailDomain}}">
</div>
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
{{end}}
<!-- OAuth2 -->
@ -288,13 +274,6 @@
<label for="open_id_connect_auto_discovery_url">{{ctx.Locale.Tr "admin.auths.openIdConnectAutoDiscoveryURL"}}</label>
<input id="open_id_connect_auto_discovery_url" name="open_id_connect_auto_discovery_url" value="{{$cfg.OpenIDConnectAutoDiscoveryURL}}">
</div>
<div class="optional field">
<div class="ui checkbox">
<label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}>
<p class="help">{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}</p>
</div>
</div>
<div class="oauth2_use_custom_url inline field">
<div class="ui checkbox">
<label><strong>{{ctx.Locale.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label>

View File

@ -18,3 +18,8 @@
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
</div>
{{- end -}}
{{- if .ShowTwoFactorRequiredMessage -}}
<div class="ui negative message flash-message flash-error">
<p><a href="{{AppSubUrl}}/user/settings/security/two_factor/enroll">{{ctx.Locale.Tr "auth.twofa_required"}}</a></p>
</div>
{{- end -}}

View File

@ -2,6 +2,7 @@
<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container">
{{template "base/alert" .}}
<div class="status-page-error">
<div class="status-page-error-title">404 Not Found</div>
<div class="tw-text-center">

View File

@ -94,7 +94,7 @@ func TestBasicAuthWithWebAuthn(t *testing.T) {
}
var userParsed userResponse
DecodeJSON(t, resp, &userParsed)
assert.Equal(t, "Basic authorization is not allowed while webAuthn enrolled", userParsed.Message)
assert.Equal(t, "basic authorization is not allowed while WebAuthn enrolled", userParsed.Message)
// user32 has webauthn enrolled, he can't request git protocol with basic auth
req = NewRequest(t, "GET", "/user2/repo1/info/refs")