Make LDAP be able to skip local 2FA ()

This PR extends  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>
This commit is contained in:
zeripath 2021-09-17 12:43:47 +01:00 committed by GitHub
parent f96d0d3d5b
commit 27b351aba5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 84 additions and 19 deletions

View file

@ -89,6 +89,10 @@ var (
Name: "public-ssh-key-attribute",
Usage: "The attribute of the users LDAP record containing the users 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,
@ -245,6 +249,10 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("allow-deactivate-all") {
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
}
if c.IsSet("skip-local-2fa") {
config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
}
return nil
}

View file

@ -214,6 +214,10 @@ func (ctx *APIContext) RequireCSRF() {
// 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 {

View file

@ -151,6 +151,9 @@ func ToggleAPI(options *ToggleOptions) func(ctx *APIContext) {
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) {

View file

@ -145,6 +145,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}

View file

@ -175,7 +175,7 @@ func SignInPost(ctx *context.Context) {
}
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)
@ -201,6 +201,15 @@ func SignInPost(ctx *context.Context) {
}
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)
@ -905,7 +914,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
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
@ -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.
// 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) {

View file

@ -291,7 +291,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
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)

View file

@ -229,7 +229,7 @@ func DeleteAccount(ctx *context.Context) {
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)

View file

@ -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)
u, err := UserSignIn(uname, passwd)
u, source, err := UserSignIn(uname, passwd)
if err != nil {
if !models.IsErrUserNotExist(err) {
log.Error("UserSignIn: %v", err)
@ -115,6 +115,10 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
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

View file

@ -54,6 +54,11 @@ type PasswordAuthenticator interface {
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

View file

@ -20,24 +20,24 @@ import (
)
// 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)}
@ -45,41 +45,41 @@ func UserSignIn(username, password string) (*models.User, error) {
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 {
@ -97,7 +97,7 @@ func UserSignIn(username, password string) (*models.User, error) {
if err == nil {
if !authUser.ProhibitLogin {
return authUser, nil
return authUser, source, nil
}
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}
}

View file

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

View file

@ -52,6 +52,7 @@ type Source struct {
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

View file

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

View file

@ -13,3 +13,6 @@ import (
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

View file

@ -147,6 +147,13 @@
</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>

View file

@ -111,4 +111,17 @@
<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>