package main import ( "crypto/rand" "encoding/base64" "io" "log" "net/http" "os" "os/exec" "sync" "time" "gopkg.in/gomail.v2" ) type resetTokenEntry struct { dn string expiresAt time.Time } var resetTokenStore = struct { mu sync.Mutex tokens map[string]resetTokenEntry }{tokens: make(map[string]resetTokenEntry)} func generateResetToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } func storeResetToken(token string, dn string) { resetTokenStore.mu.Lock() defer resetTokenStore.mu.Unlock() // Clean expired tokens now := time.Now() for t, e := range resetTokenStore.tokens { if now.After(e.expiresAt) { delete(resetTokenStore.tokens, t) } } resetTokenStore.tokens[token] = resetTokenEntry{ dn: dn, expiresAt: now.Add(time.Hour), } } func consumeResetToken(token string) (string, bool) { resetTokenStore.mu.Lock() defer resetTokenStore.mu.Unlock() entry, ok := resetTokenStore.tokens[token] if !ok || time.Now().After(entry.expiresAt) { delete(resetTokenStore.tokens, token) return "", false } delete(resetTokenStore.tokens, token) return entry.dn, true } func lostPasswordToken(conn *LDAPConn, login string) (string, string, error) { // Bind as service to perform the search err := conn.ServiceBind() if err != nil { return "", "", err } // Search the dn of the given user dn, err := conn.SearchDN(login, true) if err != nil { return "", "", err } // Generate a cryptographically random token token, err := generateResetToken() if err != nil { return "", "", err } // Store token server-side with expiration storeResetToken(token, dn) return token, dn, nil } func lostPassword(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" && !lostLimiter.Allow(remoteIP(r)) { displayTmplError(w, http.StatusTooManyRequests, "lost.html", map[string]any{"error": "Too many requests. Please try again later."}) return } if r.Method != "POST" { csrfToken, err := setCSRFToken(w) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } displayTmpl(w, "lost.html", map[string]any{"csrf_token": csrfToken}) return } if !validateCSRF(r) { displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing CSRF token. Please try again."}) return } if !validateAltcha(r) { displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing altcha response. Please try again."}) return } // Connect to the LDAP server conn, err := myLDAP.Connect() if err != nil || conn == nil { log.Println(err) displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to process your request. Please try again later."}) return } // Generate the token token, dn, err := lostPasswordToken(conn, r.PostFormValue("login")) if err != nil { log.Println(err) // Return generic message to avoid user enumeration displayMsg(w, "If an account with that login exists, a password recovery email has been sent.", http.StatusOK) return } // Search the email address entries, err := conn.GetEntry(dn) if err != nil { log.Println(err) displayMsg(w, "If an account with that login exists, a password recovery email has been sent.", http.StatusOK) return } email := "" cn := "" for _, e := range entries { if e.Name == "mail" { email = e.Values[0] } if e.Name == "cn" { cn = e.Values[0] } } if email == "" { log.Println("Unable to find a valid adress for user " + dn) displayMsg(w, "If an account with that login exists, a password recovery email has been sent.", http.StatusOK) return } // Send the email m := gomail.NewMessage() m.SetHeader("From", myLDAP.MailFrom) m.SetHeader("To", email) 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") var s gomail.Sender if myLDAP.MailHost != "" { d := gomail.NewDialer(myLDAP.MailHost, myLDAP.MailPort, myLDAP.MailUser, myLDAP.MailPassword) s, err = d.Dial() if err != nil { log.Println("Unable to connect to email server: " + err.Error()) displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."}) return } } else { // Using local sendmail: delegate to the local admin sys the responsability to transport the mail s = gomail.SendFunc(func(from string, to []string, msg io.WriterTo) error { cmd := exec.Command("sendmail", "-t") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr pw, err := cmd.StdinPipe() if err != nil { return err } err = cmd.Start() if err != nil { return err } var errs [3]error _, errs[0] = m.WriteTo(pw) errs[1] = pw.Close() errs[2] = cmd.Wait() for _, err = range errs { if err != nil { return err } } return nil }) } if err := gomail.Send(s, m); err != nil { log.Println("Unable to send email: " + err.Error()) displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."}) return } displayMsg(w, "Password recovery email sent, check your inbox.", http.StatusOK) }