mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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>
This commit is contained in:
		| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user