1
0
mirror of https://gitcode.com/gitea/gitea.git synced 2025-06-19 11:18:16 +08:00

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
16 changed files with 84 additions and 19 deletions

@ -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
}

@ -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 {

@ -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) {

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

@ -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) {

@ -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)

@ -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)

@ -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

@ -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

@ -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}
}

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

@ -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

@ -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
}

@ -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

@ -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>

@ -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>