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:
parent
f96d0d3d5b
commit
27b351aba5
16 changed files with 84 additions and 19 deletions
|
@ -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 a new issue