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" "strings"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap" "code.gitea.io/gitea/services/auth/source/ldap"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -210,8 +211,8 @@ func newAuthService() *authService {
} }
} }
// parseAuthSource assigns values on authSource according to command line flags. // parseAuthSourceLdap assigns values on authSource according to command line flags.
func parseAuthSource(c *cli.Context, authSource *auth.Source) { func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
if c.IsSet("name") { if c.IsSet("name") {
authSource.Name = c.String("name") authSource.Name = c.String("name")
} }
@ -227,6 +228,7 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) {
if c.IsSet("disable-synchronize-users") { if c.IsSet("disable-synchronize-users") {
authSource.IsSyncEnabled = !c.Bool("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. // 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") { if c.IsSet("allow-deactivate-all") {
config.AllowDeactivateAll = c.Bool("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") { if c.IsSet("enable-groups") {
config.GroupsEnabled = c.Bool("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 { if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err return err
} }
@ -398,7 +397,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
return err return err
} }
parseAuthSource(c, authSource) parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err 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 { if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err return err
} }
@ -449,7 +448,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
return err return err
} }
parseAuthSource(c, authSource) parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err return err
} }

View File

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -156,7 +157,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"), OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
CustomURLMapping: customURLMapping, CustomURLMapping: customURLMapping,
IconURL: c.String("icon-url"), IconURL: c.String("icon-url"),
SkipLocalTwoFA: c.Bool("skip-local-2fa"),
Scopes: c.StringSlice("scopes"), Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"), RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"), RequiredClaimValue: c.String("required-claim-value"),
@ -189,6 +189,7 @@ func runAddOauth(c *cli.Context) error {
Name: c.String("name"), Name: c.String("name"),
IsActive: true, IsActive: true,
Cfg: config, Cfg: config,
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
}) })
} }
@ -294,6 +295,6 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source) 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") { if c.IsSet("disable-helo") {
conf.DisableHelo = c.Bool("disable-helo") conf.DisableHelo = c.Bool("disable-helo")
} }
if c.IsSet("skip-local-2fa") {
conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
}
return nil return nil
} }
@ -160,6 +157,7 @@ func runAddSMTP(c *cli.Context) error {
Name: c.String("name"), Name: c.String("name"),
IsActive: active, IsActive: active,
Cfg: &smtpConfig, 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.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source) 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. ;; On user registration, record the IP address and user agent of the user to help identify potential abuse.
;; RECORD_USER_SIGNUP_METADATA = false ;; 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 // Config represents login config as far as the db is concerned
type Config interface { type Config interface {
convert.Conversion 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 // SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
@ -104,11 +113,6 @@ 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. // Source represents an external way for authorizing users.
type Source struct { type Source struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -116,7 +120,8 @@ type Source struct {
Name string `xorm:"UNIQUE"` Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg convert.Conversion `xorm:"TEXT"` TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
Cfg Config `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) {
return return
} }
source.Cfg = constructor() source.Cfg = constructor()
if settable, ok := source.Cfg.(SourceSettable); ok { source.Cfg.SetAuthSource(source)
settable.SetAuthSource(source)
}
} }
} }
@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool {
return ok && skipVerifiable.IsSkipVerify() return ok && skipVerifiable.IsSkipVerify()
} }
func (source *Source) TwoFactorShouldSkip() bool {
return source.TwoFactorPolicy == "skip"
}
// CreateSource inserts a AuthSource in the DB if not already // CreateSource inserts a AuthSource in the DB if not already
// existing with the given name. // existing with the given name.
func CreateSource(ctx context.Context, source *Source) error { func CreateSource(ctx context.Context, source *Source) error {
@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error {
return nil return nil
} }
if settable, ok := source.Cfg.(SourceSettable); ok { source.Cfg.SetAuthSource(source)
settable.SetAuthSource(source)
}
registerableSource, ok := source.Cfg.(RegisterableSource) registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok { if !ok {
@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
return nil return nil
} }
if settable, ok := source.Cfg.(SourceSettable); ok { source.Cfg.SetAuthSource(source)
settable.SetAuthSource(source)
}
registerableSource, ok := source.Cfg.(RegisterableSource) registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok { if !ok {

View File

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

View File

@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
} }
return nil 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(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode), newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable), 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 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) 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" CSRFCookieName = "_csrf"
CSRFCookieHTTPOnly = true CSRFCookieHTTPOnly = true
RecordUserSignupMetadata = false RecordUserSignupMetadata = false
TwoFactorAuthEnforced = false
) )
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set // 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) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) 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") InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
if InstallLock && InternalToken == "" { if InstallLock && InternalToken == "" {
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate // 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_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_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_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_userpass = Sign In
login_openid = OpenID login_openid = OpenID
oauth_signup_tab = Register New Account oauth_signup_tab = Register New Account

View File

@ -64,6 +64,7 @@
package v1 package v1
import ( import (
gocontext "context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -210,6 +211,14 @@ func repoAssignment() func(ctx *context.APIContext) {
return return
} }
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else {
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if needTwoFactor {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else { } else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil { if err != nil {
@ -217,6 +226,7 @@ func repoAssignment() func(ctx *context.APIContext) {
return return
} }
} }
}
if !ctx.Repo.Permission.HasAnyUnitAccess() { if !ctx.Repo.Permission.HasAnyUnitAccess() {
ctx.APIErrorNotFound() ctx.APIErrorNotFound()
@ -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) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { 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/auth/source/sspi"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"xorm.io/xorm/convert"
) )
const ( const (
@ -149,7 +147,6 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
RestrictedFilter: form.RestrictedFilter, RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll, AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true, Enabled: true,
SkipLocalTwoFA: form.SkipLocalTwoFA,
} }
} }
@ -163,7 +160,6 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
SkipVerify: form.SkipVerify, SkipVerify: form.SkipVerify,
HeloHostname: form.HeloHostname, HeloHostname: form.HeloHostname,
DisableHelo: form.DisableHelo, DisableHelo: form.DisableHelo,
SkipLocalTwoFA: form.SkipLocalTwoFA,
} }
} }
@ -198,7 +194,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
Scopes: scopes, Scopes: scopes,
RequiredClaimName: form.Oauth2RequiredClaimName, RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue, RequiredClaimValue: form.Oauth2RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocalTwoFA,
GroupClaimName: form.Oauth2GroupClaimName, GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup, RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup, AdminGroup: form.Oauth2AdminGroup,
@ -252,7 +247,7 @@ func NewAuthSourcePost(ctx *context.Context) {
ctx.Data["SSPIDefaultLanguage"] = "" ctx.Data["SSPIDefaultLanguage"] = ""
hasTLS := false hasTLS := false
var config convert.Conversion var config auth.Config
switch auth.Type(form.Type) { switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP: case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form) config = parseLDAPConfig(form)
@ -264,7 +259,6 @@ func NewAuthSourcePost(ctx *context.Context) {
config = &pam_service.Source{ config = &pam_service.Source{
ServiceName: form.PAMServiceName, ServiceName: form.PAMServiceName,
EmailDomain: form.PAMEmailDomain, EmailDomain: form.PAMEmailDomain,
SkipLocalTwoFA: form.SkipLocalTwoFA,
} }
case auth.OAuth2: case auth.OAuth2:
config = parseOAuth2Config(form) config = parseOAuth2Config(form)
@ -306,6 +300,7 @@ func NewAuthSourcePost(ctx *context.Context) {
Name: form.Name, Name: form.Name,
IsActive: form.IsActive, IsActive: form.IsActive,
IsSyncEnabled: form.IsSyncEnabled, IsSyncEnabled: form.IsSyncEnabled,
TwoFactorPolicy: form.TwoFactorPolicy,
Cfg: config, Cfg: config,
}); err != nil { }); err != nil {
if auth.IsErrSourceAlreadyExist(err) { if auth.IsErrSourceAlreadyExist(err) {
@ -384,7 +379,7 @@ func EditAuthSourcePost(ctx *context.Context) {
return return
} }
var config convert.Conversion var config auth.Config
switch auth.Type(form.Type) { switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP: case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form) config = parseLDAPConfig(form)
@ -421,6 +416,7 @@ func EditAuthSourcePost(ctx *context.Context) {
source.IsActive = form.IsActive source.IsActive = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config source.Cfg = config
source.TwoFactorPolicy = form.TwoFactorPolicy
if err := auth.UpdateSource(ctx, source); err != nil { if err := auth.UpdateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) { if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true ctx.Data["Err_Name"] = true

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -163,6 +164,7 @@ func EnrollTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID) t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil { if t != nil {
@ -194,6 +196,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm) form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true ctx.Data["PageIsSettingsSecurity"] = true
ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID) t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil { if t != nil {
@ -246,6 +249,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return 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 // 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 we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil { 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) 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. // 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 // 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 return
} }

View File

@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn" wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -120,7 +121,7 @@ func WebauthnRegisterPost(ctx *context.Context) {
return return
} }
_ = ctx.Session.Delete("webauthnName") _ = ctx.Session.Delete("webauthnName")
_ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
ctx.JSON(http.StatusCreated, cred) 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 return nil, err
} }
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { if !source.TwoFactorShouldSkip() {
// Check if the user has webAuthn registration // Check if the user has WebAuthn registration
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID) hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if hasWebAuthn { 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 { 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) 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 // SynchronizableSource represents a source that can synchronize users
type SynchronizableSource interface { type SynchronizableSource interface {
Sync(ctx context.Context, updateExisting bool) error Sync(ctx context.Context, updateExisting bool) error

View File

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

View File

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

View File

@ -24,6 +24,8 @@ import (
// Source Basic LDAP authentication service // Source Basic LDAP authentication service
type Source struct { type Source struct {
auth.ConfigBase `json:"-"`
Name string // canonical name (ie. corporate.ad) Name string // canonical name (ie. corporate.ad)
Host string // LDAP host Host string // LDAP host
Port int // port number Port int // port number
@ -54,9 +56,6 @@ type Source struct {
GroupTeamMap string // Map LDAP groups to teams 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 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 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. // FromDB fills up a LDAPConfig from serialized format.
@ -109,11 +108,6 @@ func (source *Source) ProvidesSSHKeys() bool {
return strings.TrimSpace(source.AttributeSSHPublicKey) != "" return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
} }
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() { func init() {
auth.RegisterTypeConfig(auth.LDAP, &Source{}) auth.RegisterTypeConfig(auth.LDAP, &Source{})
auth.RegisterTypeConfig(auth.DLDAP, &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 { if user != nil {
loginName = user.LoginName 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 { if sr == nil {
// User not in LDAP, do nothing // User not in LDAP, do nothing
return nil, user_model.ErrUserNotExist{Name: loginName} 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 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 { if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err return user, err
} }
@ -84,8 +84,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
Name: sr.Username, Name: sr.Username,
FullName: composeFullName(sr.Name, sr.Surname, sr.Username), FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
Email: sr.Mail, Email: sr.Mail,
LoginType: source.authSource.Type, LoginType: source.AuthSource.Type,
LoginSource: source.authSource.ID, LoginSource: source.AuthSource.ID,
LoginName: userName, LoginName: userName,
IsAdmin: sr.IsAdmin, IsAdmin: sr.IsAdmin,
} }
@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, err 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 { if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err return user, err
} }
@ -123,8 +123,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil 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 // Sync causes this ldap source to synchronize its users with the db
func (source *Source) Sync(ctx context.Context, updateExisting bool) error { 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) != "" isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
var sshKeysNeedUpdate bool var sshKeysNeedUpdate bool
// Find all users with this login type - FIXME: Should this be an iterator? // 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 { if err != nil {
log.Error("SyncExternalUsers: %v", err) log.Error("SyncExternalUsers: %v", err)
return err return err
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Warn("SyncExternalUsers: Cancelled 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) return db.ErrCancelledf("Before update of %s", source.AuthSource.Name)
default: default:
} }
@ -51,7 +51,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
sr, err := source.SearchEntries() sr, err := source.SearchEntries()
if err != nil { 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 return nil
} }
@ -74,7 +74,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
for _, su := range sr { for _, su := range sr {
select { select {
case <-ctx.Done(): 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 // Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
if sshKeysNeedUpdate { if sshKeysNeedUpdate {
err = asymkey_service.RewriteAllPublicKeys(ctx) err = asymkey_service.RewriteAllPublicKeys(ctx)
@ -82,7 +82,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Error("RewriteAllPublicKeys: %v", err) 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: default:
} }
if su.Username == "" && su.Mail == "" { 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) fullName := composeFullName(su.Name, su.Surname, su.Username)
// If no existing user found, create one // If no existing user found, create one
if usr == nil { 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{ usr = &user_model.User{
LowerName: su.LowerName, LowerName: su.LowerName,
Name: su.Username, Name: su.Username,
FullName: fullName, FullName: fullName,
LoginType: source.authSource.Type, LoginType: source.AuthSource.Type,
LoginSource: source.authSource.ID, LoginSource: source.AuthSource.ID,
LoginName: su.Username, LoginName: su.Username,
Email: su.Mail, Email: su.Mail,
IsAdmin: su.IsAdmin, 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) err = user_model.CreateUser(ctx, usr, &user_model.Meta{}, overwriteDefault)
if err != nil { 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 { if err == nil && isAttributeSSHPublicKeySet {
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.authSource.Name, usr.Name) 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) { if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
sshKeysNeedUpdate = true sshKeysNeedUpdate = true
} }
} }
@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
} }
} else if updateExisting { } else if updateExisting {
// Synchronize SSH Public Key if that attribute is set // 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 sshKeysNeedUpdate = true
} }
@ -155,7 +155,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
!strings.EqualFold(usr.Email, su.Mail) || !strings.EqualFold(usr.Email, su.Mail) ||
usr.FullName != fullName || usr.FullName != fullName ||
!usr.IsActive { !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{ opts := &user_service.UpdateOptions{
FullName: optional.Some(fullName), 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 { 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 { 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 { select {
case <-ctx.Done(): case <-ctx.Done():
log.Warn("SyncExternalUsers: Cancelled 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) return db.ErrCancelledf("During update of %s before delete users", source.AuthSource.Name)
default: default:
} }
@ -214,13 +214,13 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
continue 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{ opts := &user_service.UpdateOptions{
IsActive: optional.Some(false), IsActive: optional.Some(false),
} }
if err := user_service.UpdateUser(ctx, usr, opts); err != nil { 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 { type sourceInterface interface {
auth_model.Config auth_model.Config
auth_model.SourceSettable
auth_model.RegisterableSource auth_model.RegisterableSource
auth.PasswordAuthenticator auth.PasswordAuthenticator
} }

View File

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

View File

@ -13,7 +13,7 @@ import (
// Callout redirects request/response pair to authenticate against the provider // Callout redirects request/response pair to authenticate against the provider
func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error { func (source *Source) Callout(request *http.Request, response http.ResponseWriter) error {
// not sure if goth is thread safe (?) when using multiple providers // 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 // 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 // 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 // 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) { func (source *Source) Callback(request *http.Request, response http.ResponseWriter) (goth.User, error) {
// not sure if goth is thread safe (?) when using multiple providers // 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() gothRWMutex.RLock()
defer gothRWMutex.RUnlock() defer gothRWMutex.RUnlock()

View File

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

View File

@ -18,27 +18,27 @@ import (
// Sync causes this OAuth2 source to synchronize its users with the db. // Sync causes this OAuth2 source to synchronize its users with the db.
func (source *Source) Sync(ctx context.Context, updateExisting bool) error { 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 { 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 return nil
} }
provider, err := createProvider(source.authSource.Name, source) provider, err := createProvider(source.AuthSource.Name, source)
if err != nil { if err != nil {
return err return err
} }
if !provider.RefreshTokenAvailable() { 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 return nil
} }
opts := user_model.FindExternalUserOptions{ opts := user_model.FindExternalUserOptions{
HasRefreshToken: true, HasRefreshToken: true,
Expired: 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 { 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 // recognizes them as a valid user, they will be able to login
// via their provider and reactivate their account. // via their provider and reactivate their account.
if shouldDisable { 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 { return db.WithTx(ctx, func(ctx context.Context) error {
if hasUser { if hasUser {

View File

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

View File

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

View File

@ -17,12 +17,10 @@ import (
// Source holds configuration for the PAM login source. // Source holds configuration for the PAM login source.
type Source struct { type Source struct {
auth.ConfigBase `json:"-"`
ServiceName string // pam service (e.g. system-auth) ServiceName string // pam service (e.g. system-auth)
EmailDomain string EmailDomain string
SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
// reference to the authSource
authSource *auth.Source
} }
// FromDB fills up a PAMConfig from serialized format. // FromDB fills up a PAMConfig from serialized format.
@ -35,11 +33,6 @@ func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source) return json.Marshal(source)
} }
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() { func init() {
auth.RegisterTypeConfig(auth.PAM, &Source{}) 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, Email: email,
Passwd: password, Passwd: password,
LoginType: auth.PAM, LoginType: auth.PAM,
LoginSource: source.authSource.ID, LoginSource: source.AuthSource.ID,
LoginName: userName, // This is what the user typed in LoginName: userName, // This is what the user typed in
} }
overwriteDefault := &user_model.CreateUserOverwriteOptions{ overwriteDefault := &user_model.CreateUserOverwriteOptions{
@ -69,8 +69,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil 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.SkipVerifiable
auth_model.HasTLSer auth_model.HasTLSer
auth_model.UseTLSer auth_model.UseTLSer
auth_model.SourceSettable
} }
var _ (sourceInterface) = &smtp.Source{} var _ (sourceInterface) = &smtp.Source{}

View File

@ -17,6 +17,8 @@ import (
// Source holds configuration for the SMTP login source. // Source holds configuration for the SMTP login source.
type Source struct { type Source struct {
auth.ConfigBase `json:"-"`
Auth string Auth string
Host string Host string
Port int Port int
@ -25,10 +27,6 @@ type Source struct {
SkipVerify bool SkipVerify bool
HeloHostname string HeloHostname string
DisableHelo bool DisableHelo bool
SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
} }
// FromDB fills up an SMTPConfig from serialized format. // FromDB fills up an SMTPConfig from serialized format.
@ -56,11 +54,6 @@ func (source *Source) UseTLS() bool {
return source.ForceSMTPS || source.Port == 465 return source.ForceSMTPS || source.Port == 465
} }
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
}
func init() { func init() {
auth.RegisterTypeConfig(auth.SMTP, &Source{}) 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, Email: userName,
Passwd: password, Passwd: password,
LoginType: auth_model.SMTP, LoginType: auth_model.SMTP,
LoginSource: source.authSource.ID, LoginSource: source.AuthSource.ID,
LoginName: userName, LoginName: userName,
} }
overwriteDefault := &user_model.CreateUserOverwriteOptions{ overwriteDefault := &user_model.CreateUserOverwriteOptions{
@ -85,8 +85,3 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
return user, nil 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. // Source holds configuration for SSPI single sign-on.
type Source struct { type Source struct {
auth.ConfigBase `json:"-"`
AutoCreateUsers bool AutoCreateUsers bool
AutoActivateUsers bool AutoActivateUsers bool
StripDomainNames bool StripDomainNames bool

View File

@ -196,6 +196,8 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["SystemConfig"] = setting.Config() 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 // 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["DisableMigrations"] = setting.Repository.DisableMigrations
ctx.Data["DisableStars"] = setting.Repository.DisableStars 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. // HasError returns true if error occurs in form validation.
// Attention: this function changes ctx.Data and ctx.Flash // 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. // If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.

View File

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

View File

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

View File

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

View File

@ -18,3 +18,8 @@
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p> <p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
</div> </div>
{{- end -}} {{- 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}}"> <div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}} {{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}}
<div class="status-page-error"> <div class="status-page-error">
<div class="status-page-error-title">404 Not Found</div> <div class="status-page-error-title">404 Not Found</div>
<div class="tw-text-center"> <div class="tw-text-center">

View File

@ -94,7 +94,7 @@ func TestBasicAuthWithWebAuthn(t *testing.T) {
} }
var userParsed userResponse var userParsed userResponse
DecodeJSON(t, resp, &userParsed) 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 // user32 has webauthn enrolled, he can't request git protocol with basic auth
req = NewRequest(t, "GET", "/user2/repo1/info/refs") req = NewRequest(t, "GET", "/user2/repo1/info/refs")