Add LDAP authentication
- LDAP bind + search auth with auto-create local user - Falls back to local auth if LDAP not configured or fails - Configurable via MH_LDAP_* environment variables - Supports ldap:// and ldaps:// with optional TLS skip - go-ldap/ldap/v3 dependency added
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type LDAPConfig struct {
|
||||
URL string // ldap://host:389 or ldaps://host:636
|
||||
BindDN string // cn=admin,dc=example,dc=com (for search)
|
||||
BindPass string
|
||||
BaseDN string // dc=example,dc=com
|
||||
UserFilter string // (&(objectClass=inetOrgPerson)(uid=%s))
|
||||
SkipTLS bool
|
||||
}
|
||||
|
||||
func LDAPConfigFromEnv() *LDAPConfig {
|
||||
url := os.Getenv("MH_LDAP_URL")
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
filter := os.Getenv("MH_LDAP_USER_FILTER")
|
||||
if filter == "" {
|
||||
filter = "(&(objectClass=inetOrgPerson)(uid=%s))"
|
||||
}
|
||||
return &LDAPConfig{
|
||||
URL: url,
|
||||
BindDN: os.Getenv("MH_LDAP_BIND_DN"),
|
||||
BindPass: os.Getenv("MH_LDAP_BIND_PASS"),
|
||||
BaseDN: os.Getenv("MH_LDAP_BASE_DN"),
|
||||
UserFilter: filter,
|
||||
SkipTLS: os.Getenv("MH_LDAP_SKIP_TLS") == "true",
|
||||
}
|
||||
}
|
||||
|
||||
// LDAPAuth attempts to authenticate a user via LDAP.
|
||||
// Returns (email, displayName, error).
|
||||
func LDAPAuth(cfg *LDAPConfig, username, password string) (string, string, error) {
|
||||
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: cfg.SkipTLS}))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("ldap connect: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind with service account to search
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.BindPass); err != nil {
|
||||
return "", "", fmt.Errorf("ldap bind: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Search for user
|
||||
filter := fmt.Sprintf(cfg.UserFilter, ldap.EscapeFilter(username))
|
||||
sr, err := conn.Search(ldap.NewSearchRequest(
|
||||
cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 10, false,
|
||||
filter, []string{"dn", "mail", "cn", "uid"}, nil,
|
||||
))
|
||||
if err != nil || len(sr.Entries) == 0 {
|
||||
return "", "", fmt.Errorf("ldap user not found")
|
||||
}
|
||||
|
||||
entry := sr.Entries[0]
|
||||
userDN := entry.DN
|
||||
|
||||
// Bind as user to verify password
|
||||
if err := conn.Bind(userDN, password); err != nil {
|
||||
return "", "", fmt.Errorf("ldap auth failed")
|
||||
}
|
||||
|
||||
email := entry.GetAttributeValue("mail")
|
||||
if email == "" {
|
||||
email = username + "@ldap"
|
||||
}
|
||||
displayName := entry.GetAttributeValue("cn")
|
||||
if displayName == "" {
|
||||
displayName = entry.GetAttributeValue("uid")
|
||||
}
|
||||
|
||||
return email, displayName, nil
|
||||
}
|
||||
Reference in New Issue
Block a user