mirror of
https://github.com/thomiceli/opengist.git
synced 2025-06-23 02:07:58 +02:00
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:
64
internal/auth/ldap/ldap.go
Normal file
64
internal/auth/ldap/ldap.go
Normal 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
|
||||
}
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user