# HG changeset patch # User Sascha L. Teichmann # Date 1533135460 -7200 # Node ID 4bee4ba6dc5829a9d91ce93f3fe65af18a54916a # Parent 0d2bdec1e637d70ca87500005c177feebf9b3e69 Password reset: Part III diff -r 0d2bdec1e637 -r 4bee4ba6dc58 controllers/pwreset.go --- a/controllers/pwreset.go Wed Aug 01 16:53:31 2018 +0200 +++ b/controllers/pwreset.go Wed Aug 01 16:57:40 2018 +0200 @@ -1,13 +1,16 @@ package controllers import ( + "bytes" "database/sql" "encoding/hex" - "fmt" "log" + "math/big" "net/http" + "os/exec" "strings" "sync" + "text/template" "time" "gemma.intevation.de/gemma/auth" @@ -21,6 +24,9 @@ userExistsSQL = `SELECT email_address FROM users.list_users WHERE username = $1 LIMIT 1` + + updatePasswordSQL = `UPDATE users.list_users + SET pw = $1 WHERE username = $2` ) const ( @@ -29,23 +35,38 @@ maxPasswordRequestsPerUser = 5 ) -const ( - mailTmpl = `You have requested a password change for your account on -%s://%s +var ( + passwordResetRequestMailTmpl = template.Must( + template.New("request").Parse(`You have requested a password change +for your account {{ .User }} on +{{ .HTTPS }}://{{ .Server }} Please follow this link to get to the page where you can change your password. -%s://%s/#/api/users/passwordreset/%s +{{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} The link is only valid for one hour. Best regards - Your service team` + Your service team`)) + + passwordResetMailTmpl = template.Must( + template.New("reset").Parse(`Your password for your account {{ .User }} on +{{ .HTTPS }}://{{ .Server }} + +has been changed to + {{ .Password }} + +Change it as soon as possible. + +Best regards + Your service team`)) ) type timedUser struct { - user string - time time.Time + user string + email string + time time.Time } type resetRequests struct { @@ -85,25 +106,25 @@ return true } -func (rr *resetRequests) store(hash, user string) { +func (rr *resetRequests) store(hash, user, email string) { now := time.Now() rr.Lock() - rr.reqs[hash] = &timedUser{user, now} + rr.reqs[hash] = &timedUser{user, email, now} rr.Unlock() } -func (rr *resetRequests) fetch(hash string) (string, bool) { +func (rr *resetRequests) fetch(hash string) *timedUser { rr.Lock() defer rr.Unlock() tu := rr.reqs[hash] if tu == nil { - return "", false + return nil } if tu.time.Before(time.Now().Add(-passwordResetValid)) { delete(rr.reqs, hash) - return "", false + return nil } - return tu.user, true + return tu } func (rr *resetRequests) delete(hash string) { @@ -123,28 +144,121 @@ } } -func messageBody(https bool, hash, serverName string) string { - - var proto string - - if https { - proto = "https" - } else { - proto = "http" +func requestMessageBody(https, user, hash, server string) string { + var content = struct { + User string + HTTPS string + Server string + Hash string + }{ + User: user, + HTTPS: https, + Server: server, + Hash: hash, } - - return fmt.Sprintf(mailTmpl, - proto, serverName, - proto, serverName, - hash) + var buf bytes.Buffer + if err := passwordResetRequestMailTmpl.Execute(&buf, &content); err != nil { + log.Printf("error: %v\n", err) + } + return buf.String() } -const hashLength = 32 +func changedMessageBody(https, user, password, server string) string { + var content = struct { + User string + HTTPS string + Server string + Password string + }{ + User: user, + HTTPS: https, + Server: server, + Password: password, + } + var buf bytes.Buffer + if err := passwordResetMailTmpl.Execute(&buf, &content); err != nil { + log.Printf("error: %v\n", err) + } + return buf.String() +} + +func useHTTPS(req *http.Request) string { + if strings.ToLower(req.URL.Scheme) == "https" { + return "https" + } + return "http" +} + +const ( + hashLength = 32 + passwordLength = 20 +) func generateHash() string { return hex.EncodeToString(auth.GenerateRandomKey(hashLength)) } +const ( + base62alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + special = ",.!;" +) + +var ( + zero = big.NewInt(0) + div62 = big.NewInt(62) +) + +func encodeToString(src []byte, max int) string { + v := new(big.Int) + v.SetBytes(src[1:]) + m := new(big.Int) + z := new(big.Int) + out := make([]byte, 0, max) + for { + z.DivMod(v, div62, m) + // reverse order but it doesnt matter. + out = append(out, base62alphabet[m.Int64()]) + if len(out) == max-1 || z.Cmp(zero) == 0 { + break + } + v, z = z, v + } + out = append(out, special[int(src[0])%len(special)]) + return string(out) +} + +func generateNewPassword() string { + // First try pwgen + out, err := exec.Command("pwgen", "-y", "20", "1").Output() + if err == nil { + return strings.TrimSpace(string(out)) + } + + // Use internal base62 encoder. + return encodeToString(auth.GenerateRandomKey(20), 20) + +} + +func sendMail(email, subject, body string) error { + cfg := &config.Config + m := gomail.NewMessage() + m.SetHeader("From", cfg.MailFrom) + m.SetHeader("To", email) + m.SetHeader("Subject", subject) + m.SetBody("text/plain", body) + + d := gomail.Dialer{ + Host: cfg.MailHost, + Port: int(cfg.MailPort), + Username: cfg.MailUser, + Password: cfg.MailPassword, + LocalName: cfg.MailHelo, + SSL: cfg.MailPort == 465, + } + + return d.DialAndSend(m) +} + func passwordResetRequest( input interface{}, req *http.Request, @@ -168,7 +282,6 @@ } cfg := &config.Config - if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { return } @@ -196,37 +309,18 @@ hash := generateHash() - passwordResetRequests.store(hash, user.User) - - serverName := req.Host - useHTTPS := strings.ToLower(req.URL.Scheme) == "https" - - body := messageBody(useHTTPS, hash, serverName) + passwordResetRequests.store(hash, user.User, email) - m := gomail.NewMessage() - m.SetHeader("From", cfg.MailFrom) - m.SetHeader("To", email) - m.SetHeader("Subject", "Password Reset Link") - m.SetBody("text/plain", body) + body := requestMessageBody(useHTTPS(req), user.User, hash, req.Host) - d := gomail.Dialer{ - Host: cfg.MailHost, - Port: int(cfg.MailPort), - Username: cfg.MailUser, - Password: cfg.MailPassword, - LocalName: cfg.MailHelo, - SSL: cfg.MailPort == 465, - } - - if err = d.DialAndSend(m); err != nil { + if err = sendMail(email, "Password Reset Link", body); err != nil { return } jr.Result = &struct { SendTo string `json:"send-to"` - }{ - SendTo: email, - } + }{email} + return } @@ -242,15 +336,47 @@ return } - user, ok := passwordResetRequests.fetch(hash) - if !ok { + tu := passwordResetRequests.fetch(hash) + if tu == nil { err = JSONError{http.StatusNotFound, "No such hash"} return } - log.Printf("password reset for %s\n", user) + password := generateNewPassword() + + cfg := &config.Config + if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { + return + } + defer db.Close() + + var res sql.Result + if res, err = db.Exec( + updatePasswordSQL, + password, + tu.user, + ); err != nil { + return + } - // TODO: Reset PW and send email. + passwordResetRequests.delete(hash) + + if n, err2 := res.RowsAffected(); err2 != nil && n == 0 { + err = JSONError{ + Code: http.StatusNotFound, + Message: "User not found", + } + return + } + + body := changedMessageBody(useHTTPS(req), tu.user, password, req.Host) + if err = sendMail(tu.email, "Password Reset Done", body); err != nil { + return + } + + jr.Result = &struct { + Message string `json:"message"` + }{"User password changed"} return }