Compare commits

..

No commits in common. "e6bca3ac8fdf9e34ce618ad7f51689ab74be42b2" and "71805cf65ccf8079a03d4473d7f76daf9b6d4e6e" have entirely different histories.

7 changed files with 69 additions and 123 deletions

View file

@ -115,7 +115,6 @@ func addyAliasAPI(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer conn.Close()
err = conn.ServiceBind() err = conn.ServiceBind()
if err != nil { if err != nil {
@ -198,7 +197,6 @@ func addyAliasAPIDelete(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer conn.Close()
err = conn.ServiceBind() err = conn.ServiceBind()
if err != nil { if err != nil {

View file

@ -67,51 +67,31 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
// Check the two new passwords are identical // Check the two new passwords are identical
if r.PostFormValue("newpassword") != r.PostFormValue("new2password") { if r.PostFormValue("newpassword") != r.PostFormValue("new2password") {
renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.") renderError(http.StatusNotAcceptable, "New passwords are not identical. Please retry.")
return } else if len(r.PostFormValue("login")) == 0 {
}
if len(r.PostFormValue("login")) == 0 {
renderError(http.StatusNotAcceptable, "Please provide a valid login") renderError(http.StatusNotAcceptable, "Please provide a valid login")
return } else if err := checkPasswdConstraint(r.PostFormValue("newpassword")); err != nil {
}
if err := checkPasswdConstraint(r.PostFormValue("newpassword")); err != nil {
renderError(http.StatusNotAcceptable, "The password you chose doesn't respect all constraints: "+err.Error()) renderError(http.StatusNotAcceptable, "The password you chose doesn't respect all constraints: "+err.Error())
return } else {
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else if err := conn.ServiceBind(); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else if dn, err := conn.SearchDN(r.PostFormValue("login"), true); err != nil {
log.Println(err)
// User not found: perform a dummy bind to prevent username enumeration via timing.
conn.Bind("cn=dummy,"+myLDAP.BaseDN, r.PostFormValue("password"))
renderError(http.StatusUnauthorized, "Invalid login or password.")
} else if err := conn.Bind(dn, r.PostFormValue("password")); err != nil {
log.Println(err)
renderError(http.StatusUnauthorized, "Invalid login or password.")
} else if err := conn.ChangePassword(dn, r.PostFormValue("newpassword")); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
} else {
displayMsg(w, "Password successfully changed!", http.StatusOK)
}
} }
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
return
}
defer conn.Close()
if err := conn.ServiceBind(); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
return
}
dn, err := conn.SearchDN(r.PostFormValue("login"), true)
if err != nil {
log.Println(err)
// User not found: perform a dummy bind to prevent username enumeration via timing.
conn.Bind("cn=dummy,"+myLDAP.BaseDN, r.PostFormValue("password"))
renderError(http.StatusUnauthorized, "Invalid login or password.")
return
}
if err := conn.Bind(dn, r.PostFormValue("password")); err != nil {
log.Println(err)
renderError(http.StatusUnauthorized, "Invalid login or password.")
return
}
if err := conn.ChangePassword(dn, r.PostFormValue("newpassword")); err != nil {
log.Println(err)
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
return
}
displayMsg(w, "Password successfully changed!", http.StatusOK)
} }

82
ldap.go
View file

@ -4,10 +4,8 @@ import (
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"strconv"
"strings"
"time"
"github.com/amoghe/go-crypt" "github.com/amoghe/go-crypt"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
@ -21,45 +19,38 @@ type LDAP struct {
BaseDN string BaseDN string
ServiceDN string ServiceDN string
ServicePassword string ServicePassword string
} MailHost string
MailPort int
type SMTPConfig struct { MailUser string
MailHost string MailPassword string
MailPort int MailFrom string
MailUser string
MailPassword string
MailFrom string
} }
func (l LDAP) Connect() (*LDAPConn, error) { func (l LDAP) Connect() (*LDAPConn, error) {
addr := fmt.Sprintf("%s:%d", l.Host, l.Port)
var opts []ldap.DialOpt
if l.Ssl { if l.Ssl {
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ServerName: l.Host})) if c, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port), &tls.Config{ServerName: l.Host}); err != nil {
} return nil, errors.New("unable to establish LDAPS connection to " + fmt.Sprintf("%s:%d", l.Host, l.Port) + ": " + err.Error())
} else {
scheme := "ldap" return &LDAPConn{
if l.Ssl { LDAP: l,
scheme = "ldaps" connection: c,
} }, nil
}
c, err := ldap.DialURL(fmt.Sprintf("%s://%s", scheme, addr), opts...) } else if c, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port)); err != nil {
if err != nil { return nil, errors.New("unable to establish LDAP connection to " + fmt.Sprintf("%s:%d", l.Host, l.Port) + ": " + err.Error())
return nil, fmt.Errorf("unable to establish %s connection to %s: %w", strings.ToUpper(scheme), addr, err) } else {
} if l.Starttls {
if err = c.StartTLS(&tls.Config{ServerName: l.Host}); err != nil {
if l.Starttls { c.Close()
if err = c.StartTLS(&tls.Config{ServerName: l.Host}); err != nil { return nil, errors.New("unable to StartTLS: " + err.Error())
c.Close() }
return nil, fmt.Errorf("unable to StartTLS: %w", err)
} }
}
return &LDAPConn{ return &LDAPConn{
LDAP: l, LDAP: l,
connection: c, connection: c,
}, nil }, nil
}
} }
type LDAPConn struct { type LDAPConn struct {
@ -67,10 +58,6 @@ type LDAPConn struct {
connection *ldap.Conn connection *ldap.Conn
} }
func (l LDAPConn) Close() {
l.connection.Close()
}
func (l LDAPConn) ServiceBind() error { func (l LDAPConn) ServiceBind() error {
return l.connection.Bind(l.ServiceDN, l.ServicePassword) return l.connection.Bind(l.ServiceDN, l.ServicePassword)
} }
@ -98,11 +85,8 @@ func (l LDAPConn) SearchDN(username string, person bool) (string, error) {
return "", err return "", err
} }
if len(sr.Entries) == 0 { if len(sr.Entries) != 1 {
return "", fmt.Errorf("user %q not found", username) return "", errors.New("User does not exist or too many entries returned")
}
if len(sr.Entries) > 1 {
return "", fmt.Errorf("multiple entries (%d) found for user %q", len(sr.Entries), username)
} }
return sr.Entries[0].DN, nil return sr.Entries[0].DN, nil
@ -120,11 +104,8 @@ func (l LDAPConn) GetEntry(dn string) ([]*ldap.EntryAttribute, error) {
return nil, err return nil, err
} }
if len(sr.Entries) == 0 { if len(sr.Entries) != 1 {
return nil, fmt.Errorf("entry not found for DN %q", dn) return nil, errors.New("User does not exist or too many entries returned")
}
if len(sr.Entries) > 1 {
return nil, fmt.Errorf("multiple entries (%d) found for DN %q", len(sr.Entries), dn)
} }
return sr.Entries[0].Attributes, nil return sr.Entries[0].Attributes, nil
@ -152,7 +133,6 @@ func (l LDAPConn) ChangePassword(dn string, rawpassword string) error {
modify := ldap.NewModifyRequest(dn, nil) modify := ldap.NewModifyRequest(dn, nil)
modify.Replace("userPassword", []string{"{CRYPT}" + hashedpasswd}) modify.Replace("userPassword", []string{"{CRYPT}" + hashedpasswd})
modify.Replace("shadowLastChange", []string{strconv.FormatInt(time.Now().Unix()/86400, 10)})
return l.connection.Modify(modify) return l.connection.Modify(modify)
} }

View file

@ -78,7 +78,6 @@ func login(login string, password string) ([]*ldap.EntryAttribute, error) {
if err != nil || conn == nil { if err != nil || conn == nil {
return nil, err return nil, err
} }
defer conn.Close()
if err = conn.ServiceBind(); err != nil { if err = conn.ServiceBind(); err != nil {
return nil, err return nil, err

View file

@ -129,7 +129,6 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to process your request. Please try again later."}) displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to process your request. Please try again later."})
return return
} }
defer conn.Close()
// Generate the token // Generate the token
token, dn, err := lostPasswordToken(conn, r.PostFormValue("login")) token, dn, err := lostPasswordToken(conn, r.PostFormValue("login"))
@ -167,14 +166,14 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
// Send the email // Send the email
m := gomail.NewMessage() m := gomail.NewMessage()
m.SetHeader("From", mySMTP.MailFrom) m.SetHeader("From", myLDAP.MailFrom)
m.SetHeader("To", email) m.SetHeader("To", email)
m.SetHeader("Subject", "SSO nemunai.re: password recovery") m.SetHeader("Subject", "SSO nemunai.re: password recovery")
m.SetBody("text/plain", "Hello "+cn+"!\n\nSomeone, and we hope it's you, requested to reset your account password. \nIn order to continue, go to:\n"+myPublicURL+"/reset?l="+r.PostFormValue("login")+"&t="+token+"\n\nThis link expires in 1 hour and can only be used once.\n\nBest regards,\n-- \nnemunai.re SSO") m.SetBody("text/plain", "Hello "+cn+"!\n\nSomeone, and we hope it's you, requested to reset your account password. \nIn order to continue, go to:\n"+myPublicURL+"/reset?l="+r.PostFormValue("login")+"&t="+token+"\n\nThis link expires in 1 hour and can only be used once.\n\nBest regards,\n-- \nnemunai.re SSO")
var s gomail.Sender var s gomail.Sender
if mySMTP.MailHost != "" { if myLDAP.MailHost != "" {
d := gomail.NewDialer(mySMTP.MailHost, mySMTP.MailPort, mySMTP.MailUser, mySMTP.MailPassword) d := gomail.NewDialer(myLDAP.MailHost, myLDAP.MailPort, myLDAP.MailUser, myLDAP.MailPassword)
s, err = d.Dial() s, err = d.Dial()
if err != nil { if err != nil {
log.Println("Unable to connect to email server: " + err.Error()) log.Println("Unable to connect to email server: " + err.Error())

31
main.go
View file

@ -31,12 +31,9 @@ var dockerRegistrySecret string
var allowedAliasDomains []string var allowedAliasDomains []string
var myLDAP = LDAP{ var myLDAP = LDAP{
Host: "localhost", Host: "localhost",
Port: 389, Port: 389,
BaseDN: "dc=example,dc=com", BaseDN: "dc=example,dc=com",
}
var mySMTP = SMTPConfig{
MailPort: 587, MailPort: 587,
MailFrom: "noreply@example.com", MailFrom: "noreply@example.com",
} }
@ -118,13 +115,8 @@ func main() {
log.Fatal(err) log.Fatal(err)
} else if cnt, err := io.ReadAll(fd); err != nil { } else if cnt, err := io.ReadAll(fd); err != nil {
log.Fatal(err) log.Fatal(err)
} else { } else if err := json.Unmarshal(cnt, &myLDAP); err != nil {
if err := json.Unmarshal(cnt, &myLDAP); err != nil { log.Fatal(err)
log.Fatal(err)
}
if err := json.Unmarshal(cnt, &mySMTP); err != nil {
log.Fatal(err)
}
} }
} }
@ -164,17 +156,17 @@ func main() {
} }
if val, ok := os.LookupEnv("SMTP_HOST"); ok { if val, ok := os.LookupEnv("SMTP_HOST"); ok {
mySMTP.MailHost = val myLDAP.MailHost = val
} }
if val, ok := os.LookupEnv("SMTP_PORT"); ok { if val, ok := os.LookupEnv("SMTP_PORT"); ok {
if port, err := strconv.Atoi(val); err == nil { if port, err := strconv.Atoi(val); err == nil {
mySMTP.MailPort = port myLDAP.MailPort = port
} else { } else {
log.Println("Invalid value for SMTP_PORT:", val) log.Println("Invalid value for SMTP_PORT:", val)
} }
} }
if val, ok := os.LookupEnv("SMTP_USER"); ok { if val, ok := os.LookupEnv("SMTP_USER"); ok {
mySMTP.MailUser = val myLDAP.MailUser = val
} }
if val, ok := os.LookupEnv("SMTP_PASSWORD_FILE"); ok { if val, ok := os.LookupEnv("SMTP_PASSWORD_FILE"); ok {
if fd, err := os.Open(val); err != nil { if fd, err := os.Open(val); err != nil {
@ -184,13 +176,13 @@ func main() {
log.Fatal(err) log.Fatal(err)
} else { } else {
fd.Close() fd.Close()
mySMTP.MailPassword = string(cnt) myLDAP.MailPassword = string(cnt)
} }
} else if val, ok := os.LookupEnv("SMTP_PASSWORD"); ok { } else if val, ok := os.LookupEnv("SMTP_PASSWORD"); ok {
mySMTP.MailPassword = val myLDAP.MailPassword = val
} }
if val, ok := os.LookupEnv("SMTP_FROM"); ok { if val, ok := os.LookupEnv("SMTP_FROM"); ok {
mySMTP.MailFrom = val myLDAP.MailFrom = val
} }
if val, ok := os.LookupEnv("PUBLIC_URL"); ok { if val, ok := os.LookupEnv("PUBLIC_URL"); ok {
myPublicURL = val myPublicURL = val
@ -215,7 +207,6 @@ func main() {
if err != nil || conn == nil { if err != nil || conn == nil {
log.Fatalf("Unable to connect to LDAP: %s", err.Error()) log.Fatalf("Unable to connect to LDAP: %s", err.Error())
} }
defer conn.Close()
token, dn, err := lostPasswordToken(conn, login) token, dn, err := lostPasswordToken(conn, login)
if err != nil { if err != nil {

View file

@ -79,7 +79,6 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.") renderError(http.StatusInternalServerError, "Unable to process your request. Please try again later.")
return return
} }
defer conn.Close()
// Bind as service to perform the password change // Bind as service to perform the password change
err = conn.ServiceBind() err = conn.ServiceBind()