This PR extends #16594 to allow LDAP to be able to be set to skip local 2FA too. The technique used here would be extensible to PAM and SMTP sources.
Signed-off-by: Andrew Thornton <art27@cantab.net>
Name: "public-ssh-key-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
},
+ cli.BoolFlag{
+ Name: "skip-local-2fa",
+ Usage: "Set to true to skip local 2fa for users authenticated by this source",
+ },
}
ldapBindDnCLIFlags = append(commonLdapCLIFlags,
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")
+ }
+
return nil
}
// CheckForOTP validates OTP
func (ctx *APIContext) CheckForOTP() {
+ if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
+ return // Skip 2FA
+ }
+
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
twofa, err := models.GetTwoFactorByUID(ctx.Context.User.ID)
if err != nil {
return
}
if ctx.IsSigned && ctx.IsBasicAuth {
+ if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
+ return // Skip 2FA
+ }
twofa, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) {
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
+ SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
}
form := web.GetForm(ctx).(*forms.SignInForm)
- u, err := auth.UserSignIn(form.UserName, form.Password)
+ u, source, err := auth.UserSignIn(form.UserName, form.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
}
return
}
+
+ // Now handle 2FA:
+
+ // First of all if the source can skip local two fa we're done
+ if skipper, ok := source.Cfg.(auth.LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
+ handleSignIn(ctx, u, form.Remember)
+ return
+ }
+
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID)
return
}
- u, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
+ u, _, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.Data["user_exists"] = true
// If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page.
+ // We deliberately ignore the skip local 2fa setting here because we are linking to a previous user here
_, err := models.GetTwoFactorByUID(u.ID)
if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) {
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["OpenID"] = oid
- u, err := auth.UserSignIn(form.UserName, form.Password)
+ u, _, err := auth.UserSignIn(form.UserName, form.Password)
if err != nil {
if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
- if _, err := auth.UserSignIn(ctx.User.Name, ctx.FormString("password")); err != nil {
+ if _, _, err := auth.UserSignIn(ctx.User.Name, ctx.FormString("password")); err != nil {
if models.IsErrUserNotExist(err) {
loadAccountData(ctx)
}
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
- u, err := UserSignIn(uname, passwd)
+ u, source, err := UserSignIn(uname, passwd)
if err != nil {
if !models.IsErrUserNotExist(err) {
log.Error("UserSignIn: %v", err)
return nil
}
+ if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
+ store.GetData()["SkipLocalTwoFA"] = true
+ }
+
log.Trace("Basic Authorization: Logged in user %-v", u)
return u
Authenticate(user *models.User, login, password string) (*models.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
)
// UserSignIn validates user name and password.
-func UserSignIn(username, password string) (*models.User, error) {
+func UserSignIn(username, password string) (*models.User, *models.LoginSource, error) {
var user *models.User
if strings.Contains(username, "@") {
user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))}
// check same email
cnt, err := models.Count(user)
if err != nil {
- return nil, err
+ return nil, nil, err
}
if cnt > 1 {
- return nil, models.ErrEmailAlreadyUsed{
+ return nil, nil, models.ErrEmailAlreadyUsed{
Email: user.Email,
}
}
} else {
trimmedUsername := strings.TrimSpace(username)
if len(trimmedUsername) == 0 {
- return nil, models.ErrUserNotExist{Name: username}
+ return nil, nil, models.ErrUserNotExist{Name: username}
}
user = &models.User{LowerName: strings.ToLower(trimmedUsername)}
hasUser, err := models.GetUser(user)
if err != nil {
- return nil, err
+ return nil, nil, err
}
if hasUser {
source, err := models.GetLoginSourceByID(user.LoginSource)
if err != nil {
- return nil, err
+ return nil, nil, err
}
if !source.IsActive {
- return nil, models.ErrLoginSourceNotActived
+ return nil, nil, models.ErrLoginSourceNotActived
}
authenticator, ok := source.Cfg.(PasswordAuthenticator)
if !ok {
- return nil, models.ErrUnsupportedLoginType
+ return nil, nil, models.ErrUnsupportedLoginType
}
user, err := authenticator.Authenticate(user, username, password)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
// user could be hint to resend confirm email.
if user.ProhibitLogin {
- return nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
+ return nil, nil, models.ErrUserProhibitLogin{UID: user.ID, Name: user.Name}
}
- return user, nil
+ return user, source, nil
}
sources, err := models.AllActiveLoginSources()
if err != nil {
- return nil, err
+ return nil, nil, err
}
for _, source := range sources {
if err == nil {
if !authUser.ProhibitLogin {
- return authUser, nil
+ return authUser, source, nil
}
err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
}
}
}
- return nil, models.ErrUserNotExist{Name: username}
+ return nil, nil, models.ErrUserNotExist{Name: username}
}
type sourceInterface interface {
auth.PasswordAuthenticator
auth.SynchronizableSource
+ auth.LocalTwoFASkipper
models.SSHKeyProvider
models.LoginConfig
models.SkipVerifiable
GroupFilter string // Group Name Filter
GroupMemberUID string // Group Attribute containing array of UserUID
UserUID string // User Attribute listed in Group
+ SkipLocalTwoFA bool // Skip Local 2fa for users authenticated with this source
// reference to the loginSource
loginSource *models.LoginSource
return user, err
}
+
+// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
+func (source *Source) IsSkipLocalTwoFA() bool {
+ return source.SkipLocalTwoFA
+}
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
return db.Authenticate(user, login, password)
}
+
+// NB: Oauth2 does not implement LocalTwoFASkipper for password authentication
+// as its password authentication drops to db authentication
</div>
</div>
{{end}}
+ <div class="optional field">
+ <div class="ui checkbox">
+ <label for="skip_local_two_fa"><strong>{{.i18n.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">{{.i18n.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>{{.i18n.Tr "admin.auths.allow_deactivate_all"}}</strong></label>
<label for="search_page_size">{{.i18n.Tr "admin.auths.search_page_size"}}</label>
<input id="search_page_size" name="search_page_size" value="{{.search_page_size}}">
</div>
+ <div class="optional field">
+ <div class="ui checkbox">
+ <label for="skip_local_two_fa"><strong>{{.i18n.Tr "admin.auths.skip_local_two_fa"}}</strong></label>
+ <input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}>
+ <p class="help">{{.i18n.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>{{.i18n.Tr "admin.auths.allow_deactivate_all"}}</strong></label>
+ <input id="allow_deactivate_all" name="allow_deactivate_all" type="checkbox" {{if .allow_deactivate_all}}checked{{end}}>
+ </div>
+ </div>
</div>