Mercurial > gemma
changeset 304:69e291f26bbd
Password reset: Part II.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Wed, 01 Aug 2018 13:38:55 +0200 |
parents | 0777aa6de45b |
children | 8aa472939148 |
files | controllers/pwreset.go controllers/routes.go |
diffstat | 2 files changed, 141 insertions(+), 6 deletions(-) [+] |
line wrap: on
line diff
--- a/controllers/pwreset.go Wed Aug 01 12:29:55 2018 +0200 +++ b/controllers/pwreset.go Wed Aug 01 13:38:55 2018 +0200 @@ -7,9 +7,13 @@ "log" "net/http" "strings" + "sync" + "time" "gemma.intevation.de/gemma/auth" "gemma.intevation.de/gemma/config" + "github.com/gorilla/mux" + gomail "gopkg.in/gomail.v2" ) @@ -20,12 +24,18 @@ ) const ( + passwordResetValid = time.Hour + maxPasswordResets = 1000 + maxPasswordRequestsPerUser = 5 +) + +const ( mailTmpl = `You have requested a password change for your account on %s://%s Please follow this link to get to the page where you can change your password. -%s://%s/#/users/passwordreset/%s +%s://%s/#/api/users/passwordreset/%s The link is only valid for one hour. @@ -33,6 +43,86 @@ Your service team` ) +type timedUser struct { + user string + time time.Time +} + +type resetRequests struct { + sync.Mutex + reqs map[string]*timedUser +} + +var passwordResetRequests = func() *resetRequests { + rr := &resetRequests{reqs: map[string]*timedUser{}} + go func() { + for { + time.Sleep(2 * time.Minute) + rr.removeOutdated() + } + }() + return rr +}() + +func (rr *resetRequests) len() int { + rr.Lock() + l := len(rr.reqs) + rr.Unlock() + return l +} + +func (rr *resetRequests) userAllowed(user string) bool { + rr.Lock() + defer rr.Unlock() + var count int + for _, v := range rr.reqs { + if v.user == user { + if count++; count >= maxPasswordRequestsPerUser { + return false + } + } + } + return true +} + +func (rr *resetRequests) store(hash, user string) { + now := time.Now() + rr.Lock() + rr.reqs[hash] = &timedUser{user, now} + rr.Unlock() +} + +func (rr *resetRequests) fetch(hash string) (string, bool) { + rr.Lock() + defer rr.Unlock() + tu := rr.reqs[hash] + if tu == nil { + return "", false + } + if tu.time.Before(time.Now().Add(-passwordResetValid)) { + delete(rr.reqs, hash) + return "", false + } + return tu.user, true +} + +func (rr *resetRequests) delete(hash string) { + rr.Lock() + delete(rr.reqs, hash) + rr.Unlock() +} + +func (rr *resetRequests) removeOutdated() { + good := time.Now().Add(-passwordResetValid) + rr.Lock() + defer rr.Unlock() + for k, v := range rr.reqs { + if v.time.Before(good) { + delete(rr.reqs, k) + } + } +} + func messageBody(https bool, hash, serverName string) string { var proto string @@ -55,13 +145,20 @@ return hex.EncodeToString(auth.GenerateRandomKey(hashLength)) } -func passwordReset( +func passwordResetRequest( input interface{}, req *http.Request, db *sql.DB, ) (jr JSONResult, err error) { - log.Println("passwordreset") + // Limit total number of password requests. + if passwordResetRequests.len() >= maxPasswordResets { + err = JSONError{ + Code: http.StatusServiceUnavailable, + Message: "Too much password reset request", + } + return + } user := input.(*PWResetUser) @@ -88,8 +185,19 @@ return } + // Limit requests per user + if !passwordResetRequests.userAllowed(user.User) { + err = JSONError{ + Code: http.StatusServiceUnavailable, + Message: "Too much password reset requests for user", + } + return + } + hash := generateHash() + passwordResetRequests.store(hash, user.User) + serverName := req.Host useHTTPS := strings.ToLower(req.URL.Scheme) == "https" @@ -114,8 +222,6 @@ return } - // TODO: Keep hash/user for one hour or till resolved. - jr.Result = &struct { SendTo string `json:"send-to"` }{ @@ -123,3 +229,28 @@ } return } + +func passwordReset( + input interface{}, + req *http.Request, + db *sql.DB, +) (jr JSONResult, err error) { + + hash := mux.Vars(req)["hash"] + if _, err = hex.DecodeString(hash); err != nil { + err = JSONError{http.StatusBadRequest, "Invalid hash"} + return + } + + user, ok := passwordResetRequests.fetch(hash) + if !ok { + err = JSONError{http.StatusNotFound, "No such hash"} + return + } + + log.Printf("password reset for %s\n", user) + + // TODO: Reset PW and send email. + + return +}
--- a/controllers/routes.go Wed Aug 01 12:29:55 2018 +0200 +++ b/controllers/routes.go Wed Aug 01 13:38:55 2018 +0200 @@ -41,8 +41,12 @@ api.Handle("/users/passwordreset", &JSONHandler{ Input: func() interface{} { return new(PWResetUser) }, + Handle: passwordResetRequest, + }).Methods(http.MethodPost) + + api.Handle("/users/passwordreset/{hash}", &JSONHandler{ Handle: passwordReset, - }).Methods(http.MethodPost) + }).Methods(http.MethodGet) api.HandleFunc("/login", login). Methods(http.MethodGet, http.MethodPost)