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)