Make LDAP be able to skip local 2FA (#16954)

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>
tokarchuk/v1.17
zeripath 3 years ago committed by GitHub
parent f96d0d3d5b
commit 27b351aba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      cmd/admin_auth_ldap.go
  2. 4
      modules/context/api.go
  3. 3
      modules/context/auth.go
  4. 1
      routers/web/admin/auths.go
  5. 14
      routers/web/user/auth.go
  6. 2
      routers/web/user/auth_openid.go
  7. 2
      routers/web/user/setting/account.go
  8. 6
      services/auth/basic.go
  9. 5
      services/auth/interface.go
  10. 28
      services/auth/signin.go
  11. 1
      services/auth/source/ldap/assert_interface_test.go
  12. 1
      services/auth/source/ldap/source.go
  13. 5
      services/auth/source/ldap/source_authenticate.go
  14. 3
      services/auth/source/oauth2/source_authenticate.go
  15. 7
      templates/admin/auth/edit.tmpl
  16. 13
      templates/admin/auth/source/ldap.tmpl

@ -89,6 +89,10 @@ var (
Name: "public-ssh-key-attribute", Name: "public-ssh-key-attribute",
Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.", 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, ldapBindDnCLIFlags = append(commonLdapCLIFlags,
@ -245,6 +249,10 @@ 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")
}
return nil return nil
} }

@ -214,6 +214,10 @@ func (ctx *APIContext) RequireCSRF() {
// CheckForOTP validates OTP // CheckForOTP validates OTP
func (ctx *APIContext) CheckForOTP() { func (ctx *APIContext) CheckForOTP() {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP") otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
twofa, err := models.GetTwoFactorByUID(ctx.Context.User.ID) twofa, err := models.GetTwoFactorByUID(ctx.Context.User.ID)
if err != nil { if err != nil {

@ -151,6 +151,9 @@ func ToggleAPI(options *ToggleOptions) func(ctx *APIContext) {
return return
} }
if ctx.IsSigned && ctx.IsBasicAuth { if ctx.IsSigned && ctx.IsBasicAuth {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
twofa, err := models.GetTwoFactorByUID(ctx.User.ID) twofa, err := models.GetTwoFactorByUID(ctx.User.ID)
if err != nil { if err != nil {
if models.IsErrTwoFactorNotEnrolled(err) { if models.IsErrTwoFactorNotEnrolled(err) {

@ -145,6 +145,7 @@ 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,
} }
} }

@ -175,7 +175,7 @@ func SignInPost(ctx *context.Context) {
} }
form := web.GetForm(ctx).(*forms.SignInForm) 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 err != nil {
if models.IsErrUserNotExist(err) { if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
@ -201,6 +201,15 @@ func SignInPost(ctx *context.Context) {
} }
return 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. // If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page. // Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID) _, err = models.GetTwoFactorByUID(u.ID)
@ -905,7 +914,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
return return
} }
u, err := auth.UserSignIn(signInForm.UserName, signInForm.Password) u, _, err := auth.UserSignIn(signInForm.UserName, signInForm.Password)
if err != nil { if err != nil {
if models.IsErrUserNotExist(err) { if models.IsErrUserNotExist(err) {
ctx.Data["user_exists"] = true ctx.Data["user_exists"] = true
@ -924,6 +933,7 @@ func linkAccount(ctx *context.Context, u *models.User, gothUser goth.User, remem
// If this user is enrolled in 2FA, we can't sign the user in just yet. // If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page. // 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) _, err := models.GetTwoFactorByUID(u.ID)
if err != nil { if err != nil {
if !models.IsErrTwoFactorNotEnrolled(err) { if !models.IsErrTwoFactorNotEnrolled(err) {

@ -291,7 +291,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["OpenID"] = oid ctx.Data["OpenID"] = oid
u, err := auth.UserSignIn(form.UserName, form.Password) u, _, err := auth.UserSignIn(form.UserName, form.Password)
if err != nil { if err != nil {
if models.IsErrUserNotExist(err) { if models.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form) ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplConnectOID, &form)

@ -229,7 +229,7 @@ func DeleteAccount(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true 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) { if models.IsErrUserNotExist(err) {
loadAccountData(ctx) loadAccountData(ctx)

@ -107,7 +107,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
} }
log.Trace("Basic Authorization: Attempting SignIn for %s", uname) log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
u, err := UserSignIn(uname, passwd) u, source, err := UserSignIn(uname, passwd)
if err != nil { if err != nil {
if !models.IsErrUserNotExist(err) { if !models.IsErrUserNotExist(err) {
log.Error("UserSignIn: %v", err) log.Error("UserSignIn: %v", err)
@ -115,6 +115,10 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil return nil
} }
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
store.GetData()["SkipLocalTwoFA"] = true
}
log.Trace("Basic Authorization: Logged in user %-v", u) log.Trace("Basic Authorization: Logged in user %-v", u)
return u return u

@ -54,6 +54,11 @@ type PasswordAuthenticator interface {
Authenticate(user *models.User, login, password string) (*models.User, error) 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 // 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

@ -20,24 +20,24 @@ import (
) )
// UserSignIn validates user name and password. // 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 var user *models.User
if strings.Contains(username, "@") { if strings.Contains(username, "@") {
user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))} user = &models.User{Email: strings.ToLower(strings.TrimSpace(username))}
// check same email // check same email
cnt, err := models.Count(user) cnt, err := models.Count(user)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if cnt > 1 { if cnt > 1 {
return nil, models.ErrEmailAlreadyUsed{ return nil, nil, models.ErrEmailAlreadyUsed{
Email: user.Email, Email: user.Email,
} }
} }
} else { } else {
trimmedUsername := strings.TrimSpace(username) trimmedUsername := strings.TrimSpace(username)
if len(trimmedUsername) == 0 { if len(trimmedUsername) == 0 {
return nil, models.ErrUserNotExist{Name: username} return nil, nil, models.ErrUserNotExist{Name: username}
} }
user = &models.User{LowerName: strings.ToLower(trimmedUsername)} user = &models.User{LowerName: strings.ToLower(trimmedUsername)}
@ -45,41 +45,41 @@ func UserSignIn(username, password string) (*models.User, error) {
hasUser, err := models.GetUser(user) hasUser, err := models.GetUser(user)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if hasUser { if hasUser {
source, err := models.GetLoginSourceByID(user.LoginSource) source, err := models.GetLoginSourceByID(user.LoginSource)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if !source.IsActive { if !source.IsActive {
return nil, models.ErrLoginSourceNotActived return nil, nil, models.ErrLoginSourceNotActived
} }
authenticator, ok := source.Cfg.(PasswordAuthenticator) authenticator, ok := source.Cfg.(PasswordAuthenticator)
if !ok { if !ok {
return nil, models.ErrUnsupportedLoginType return nil, nil, models.ErrUnsupportedLoginType
} }
user, err := authenticator.Authenticate(user, username, password) user, err := authenticator.Authenticate(user, username, password)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that // WARN: DON'T check user.IsActive, that will be checked on reqSign so that
// user could be hint to resend confirm email. // user could be hint to resend confirm email.
if user.ProhibitLogin { 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() sources, err := models.AllActiveLoginSources()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for _, source := range sources { for _, source := range sources {
@ -97,7 +97,7 @@ func UserSignIn(username, password string) (*models.User, error) {
if err == nil { if err == nil {
if !authUser.ProhibitLogin { if !authUser.ProhibitLogin {
return authUser, nil return authUser, source, nil
} }
err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name} err = models.ErrUserProhibitLogin{UID: authUser.ID, Name: authUser.Name}
} }
@ -109,5 +109,5 @@ func UserSignIn(username, password string) (*models.User, error) {
} }
} }
return nil, models.ErrUserNotExist{Name: username} return nil, nil, models.ErrUserNotExist{Name: username}
} }

@ -16,6 +16,7 @@ import (
type sourceInterface interface { type sourceInterface interface {
auth.PasswordAuthenticator auth.PasswordAuthenticator
auth.SynchronizableSource auth.SynchronizableSource
auth.LocalTwoFASkipper
models.SSHKeyProvider models.SSHKeyProvider
models.LoginConfig models.LoginConfig
models.SkipVerifiable models.SkipVerifiable

@ -52,6 +52,7 @@ type Source struct {
GroupFilter string // Group Name Filter GroupFilter string // Group Name Filter
GroupMemberUID string // Group Attribute containing array of UserUID GroupMemberUID string // Group Attribute containing array of UserUID
UserUID string // User Attribute listed in Group UserUID string // User Attribute listed in Group
SkipLocalTwoFA bool // Skip Local 2fa for users authenticated with this source
// reference to the loginSource // reference to the loginSource
loginSource *models.LoginSource loginSource *models.LoginSource

@ -97,3 +97,8 @@ func (source *Source) Authenticate(user *models.User, login, password string) (*
return user, err return user, err
} }
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
func (source *Source) IsSkipLocalTwoFA() bool {
return source.SkipLocalTwoFA
}

@ -13,3 +13,6 @@ import (
func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) { func (source *Source) Authenticate(user *models.User, login, password string) (*models.User, error) {
return db.Authenticate(user, login, password) return db.Authenticate(user, login, password)
} }
// NB: Oauth2 does not implement LocalTwoFASkipper for password authentication
// as its password authentication drops to db authentication

@ -147,6 +147,13 @@
</div> </div>
</div> </div>
{{end}} {{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="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label for="allow_deactivate_all"><strong>{{.i18n.Tr "admin.auths.allow_deactivate_all"}}</strong></label> <label for="allow_deactivate_all"><strong>{{.i18n.Tr "admin.auths.allow_deactivate_all"}}</strong></label>

@ -111,4 +111,17 @@
<label for="search_page_size">{{.i18n.Tr "admin.auths.search_page_size"}}</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}}"> <input id="search_page_size" name="search_page_size" value="{{.search_page_size}}">
</div> </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> </div>

Loading…
Cancel
Save