Add LDAP authentication (#470)

* Introduce basic LDAP authentication.

* Reformat LDAP code; use ldap in Git HTTP

* lint

---------

Co-authored-by: Santhosh Raju <santhosh.raju@gmail.com>
This commit is contained in:
Thomas Miceli
2025-05-09 19:32:22 +02:00
committed by GitHub
parent 72e02700ec
commit 897dc43790
10 changed files with 284 additions and 29 deletions

View File

@ -0,0 +1,64 @@
package ldap
import (
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/thomiceli/opengist/internal/config"
)
func Enabled() bool {
return config.C.LDAPUrl != ""
}
// Authenticate attempts to authenticate a user against the configured LDAP instance.
func Authenticate(username, password string) (bool, error) {
l, err := ldap.DialURL(config.C.LDAPUrl)
if err != nil {
return false, fmt.Errorf("unable to connect to URI: %v", config.C.LDAPUrl)
}
defer func(l *ldap.Conn) {
_ = l.Close()
}(l)
// First bind with a read only user
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
searchFilter := fmt.Sprintf(config.C.LDAPSearchFilter, username)
searchRequest := ldap.NewSearchRequest(
config.C.LDAPSearchBase,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
searchFilter,
[]string{"dn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) != 1 {
return false, nil
}
// Bind as the user to verify their password
err = l.Bind(sr.Entries[0].DN, password)
if err != nil {
return false, nil
}
// Rebind as the read only user for any further queries
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
return true, nil
}

View File

@ -79,6 +79,12 @@ type config struct {
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
LDAPBindCredentials string `yaml:"ldap.bind-credentials" env:"OG_LDAP_BIND_CREDENTIALS"`
LDAPSearchBase string `yaml:"ldap.search-base" env:"OG_LDAP_SEARCH_BASE"`
LDAPSearchFilter string `yaml:"ldap.search-filter" env:"OG_LDAP_SEARCH_FILTER"`
CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"`
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`

View File

@ -3,6 +3,7 @@ package auth
import (
"errors"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/ldap"
passwordpkg "github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
@ -114,6 +115,7 @@ func ProcessLogin(ctx *context.Context) error {
return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil)
}
var user *db.User
var err error
sess := ctx.GetSession()
@ -121,26 +123,16 @@ func ProcessLogin(ctx *context.Context) error {
if err = ctx.Bind(dto); err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
password := dto.Password
var user *db.User
if user, err = db.GetUserByUsername(dto.Username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.ErrorRes(500, "Cannot get user", err)
if ldap.Enabled() {
if user, err = tryLdapLogin(ctx, dto.Username, dto.Password); err != nil {
return err
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return ctx.RedirectTo("/login")
}
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
if user == nil {
if user, err = tryDbLogin(ctx, dto.Username, dto.Password); user == nil {
return err
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return ctx.RedirectTo("/login")
}
// handle MFA
@ -168,3 +160,59 @@ func Logout(ctx *context.Context) error {
ctx.DeleteCsrfCookie()
return ctx.RedirectTo("/all")
}
func tryDbLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
return nil, ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
return user, nil
}
func tryLdapLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
ok, err := ldap.Authenticate(username, password)
if err != nil {
log.Info().Err(err).Msgf("LDAP authentication error")
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
if !ok {
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
return nil, nil
}
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
user = &db.User{
Username: username,
}
if err = user.Create(); err != nil {
log.Warn().Err(err).Msg("Cannot create user after LDAP authentication")
return nil, ctx.ErrorRes(500, "Cannot create user", err)
}
return user, nil
}
return user, nil
}

View File

@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/auth/ldap"
"github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/web/context"
"github.com/thomiceli/opengist/internal/web/handlers"
@ -112,12 +113,28 @@ func GitHttp(ctx *context.Context) error {
userToCheckPermissions = &gist.User
}
if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot verify password", err)
// ldap
ldapSuccess := false
if ldap.Enabled() {
if ok, err := ldap.Authenticate(userToCheckPermissions.Username, authPassword); !ok {
if err != nil {
log.Warn().Err(err).Msg("LDAP authentication error")
}
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
} else {
ldapSuccess = true
}
}
// password
if !ldapSuccess {
if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot verify password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
} else {
var user *db.User
@ -128,13 +145,25 @@ func GitHttp(ctx *context.Context) error {
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if ok, err := password.VerifyPassword(authPassword, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
ldapSuccess := false
if ldap.Enabled() {
if ok, err := ldap.Authenticate(user.Username, authPassword); !ok {
if err != nil {
log.Warn().Err(err).Msg("LDAP authentication error")
}
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
} else {
ldapSuccess = true
}
}
if !ldapSuccess {
if ok, err := password.VerifyPassword(authPassword, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if isInit {