diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..780897c --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,9 @@ +package auth + +import "errors" + +var ErrAuthFailed = errors.New("authentication failed") + +type Authenticator interface { + Authenticate(user, pass string) error +} diff --git a/auth/htpasswd.go b/auth/htpasswd.go new file mode 100644 index 0000000..9f0eae6 --- /dev/null +++ b/auth/htpasswd.go @@ -0,0 +1,41 @@ +package auth + +import ( + "bufio" + "os" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +type HtpasswdAuth struct { + path string +} + +func NewHtpasswdAuth(path string) *HtpasswdAuth { + return &HtpasswdAuth{path: path} +} + +func (h *HtpasswdAuth) Authenticate(user, pass string) error { + f, err := os.Open(h.path) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + continue + } + if parts[0] == user { + if bcrypt.CompareHashAndPassword([]byte(parts[1]), []byte(pass)) == nil { + return nil + } + return ErrAuthFailed + } + } + return ErrAuthFailed +} diff --git a/auth/ldap.go b/auth/ldap.go new file mode 100644 index 0000000..ef984f2 --- /dev/null +++ b/auth/ldap.go @@ -0,0 +1,80 @@ +package auth + +import ( + "fmt" + "strings" + + "github.com/go-ldap/ldap/v3" +) + +type LDAPAuth struct { + uri string + baseDN string + bindDN string + bindPass string + userFilter string + startTLS bool +} + +func NewLDAPAuth(uri, baseDN, bindDN, bindPass, userFilter string, startTLS bool) *LDAPAuth { + return &LDAPAuth{ + uri: uri, + baseDN: baseDN, + bindDN: bindDN, + bindPass: bindPass, + userFilter: userFilter, + startTLS: startTLS, + } +} + +func (l *LDAPAuth) Authenticate(user, pass string) error { + if pass == "" { + return ErrAuthFailed + } + + conn, err := ldap.DialURL(l.uri) + if err != nil { + return fmt.Errorf("ldap connect: %w", err) + } + defer conn.Close() + + if l.startTLS { + if err := conn.StartTLS(nil); err != nil { + return fmt.Errorf("ldap starttls: %w", err) + } + } + + if err := conn.Bind(l.bindDN, l.bindPass); err != nil { + return fmt.Errorf("ldap admin bind: %w", err) + } + + filter := strings.Replace(l.userFilter, "%s", ldap.EscapeFilter(user), 1) + req := ldap.NewSearchRequest( + l.baseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + filter, + []string{"dn"}, + nil, + ) + + res, err := conn.Search(req) + if err != nil { + return fmt.Errorf("ldap search: %w", err) + } + + if len(res.Entries) == 0 { + return ErrAuthFailed + } + if len(res.Entries) > 1 { + return fmt.Errorf("ldap: ambiguous user filter matched %d users", len(res.Entries)) + } + + userDN := res.Entries[0].DN + if err := conn.Bind(userDN, pass); err != nil { + return ErrAuthFailed + } + + return nil +} diff --git a/auth/password.go b/auth/password.go new file mode 100644 index 0000000..25a96e7 --- /dev/null +++ b/auth/password.go @@ -0,0 +1,18 @@ +package auth + +import "golang.org/x/crypto/bcrypt" + +type PasswordAuth struct { + hash []byte +} + +func NewPasswordAuth(hash string) *PasswordAuth { + return &PasswordAuth{hash: []byte(hash)} +} + +func (p *PasswordAuth) Authenticate(_, pass string) error { + if err := bcrypt.CompareHashAndPassword(p.hash, []byte(pass)); err != nil { + return ErrAuthFailed + } + return nil +}