comparison controllers/pwreset.go @ 321:974a5e4c0055

Persist password reset requests in database.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Thu, 02 Aug 2018 16:40:14 +0200
parents ac760b0f22a9
children 34ecfd8dc11e
comparison
equal deleted inserted replaced
320:e4bf72cda62e 321:974a5e4c0055
8 "log" 8 "log"
9 "math/big" 9 "math/big"
10 "net/http" 10 "net/http"
11 "os/exec" 11 "os/exec"
12 "strings" 12 "strings"
13 "sync"
14 "text/template" 13 "text/template"
15 "time" 14 "time"
16 15
17 "gemma.intevation.de/gemma/auth" 16 "gemma.intevation.de/gemma/auth"
18 "gemma.intevation.de/gemma/config" 17 "gemma.intevation.de/gemma/config"
20 19
21 gomail "gopkg.in/gomail.v2" 20 gomail "gopkg.in/gomail.v2"
22 ) 21 )
23 22
24 const ( 23 const (
24 insertRequestSQL = `INSERT INTO pw_reset.password_reset_requests
25 (hash, username) VALUES ($1, $2)`
26
27 countRequestsSQL = `SELECT count(*) FROM pw_reset.password_reset_requests`
28
29 countRequestsUserSQL = `SELECT count(*) FROM pw_reset.password_reset_requests
30 WHERE username = $1`
31
32 deleteRequestSQL = `DELETE FROM pw_reset.password_reset_requests
33 WHERE hash = $1`
34
35 findRequestSQL = `SELECT lu.email_address, lu.username
36 FROM pw_reset.password_reset_requests prr
37 JOIN pw_reset.list_users lu on prr.username = lu.username
38 WHERE prr.hash = $1`
39
40 cleanupRequestsSQL = `DELETE FROM pw_reset.password_reset_requests
41 WHERE issued < $1`
42
25 userExistsSQL = `SELECT email_address 43 userExistsSQL = `SELECT email_address
26 FROM pw_reset.list_users WHERE username = $1` 44 FROM pw_reset.list_users WHERE username = $1`
27 45
28 updatePasswordSQL = `UPDATE pw_reset.list_users 46 updatePasswordSQL = `UPDATE pw_reset.list_users
29 SET pw = $1 WHERE username = $2` 47 SET pw = $1 WHERE username = $2`
30 ) 48 )
31 49
32 const ( 50 const (
33 passwordResetValid = time.Hour 51 hashLength = 16
52 passwordLength = 20
53 passwordResetValid = 12 * time.Hour
34 maxPasswordResets = 1000 54 maxPasswordResets = 1000
35 maxPasswordRequestsPerUser = 5 55 maxPasswordRequestsPerUser = 5
56 cleanupPause = 15 * time.Minute
36 ) 57 )
37 58
38 var ( 59 var (
39 passwordResetRequestMailTmpl = template.Must( 60 passwordResetRequestMailTmpl = template.Must(
40 template.New("request").Parse(`You have requested a password change 61 template.New("request").Parse(`You have requested a password change
43 64
44 Please follow this link to get to the page where you can change your password. 65 Please follow this link to get to the page where you can change your password.
45 66
46 {{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} 67 {{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }}
47 68
48 The link is only valid for one hour. 69 The link is only valid for 12 hours.
49 70
50 Best regards 71 Best regards
51 Your service team`)) 72 Your service team`))
52 73
53 passwordResetMailTmpl = template.Must( 74 passwordResetMailTmpl = template.Must(
61 82
62 Best regards 83 Best regards
63 Your service team`)) 84 Your service team`))
64 ) 85 )
65 86
66 type timedUser struct { 87 func asServiceUser(fn func(*sql.DB) error) error {
67 user string 88 cfg := &config.Config
68 email string 89 db, err := auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword)
69 time time.Time 90 if err == nil {
70 } 91 defer db.Close()
71 92 err = fn(db)
72 type resetRequests struct { 93 }
73 sync.Mutex 94 return err
74 reqs map[string]*timedUser 95 }
75 } 96
76 97 func init() {
77 var passwordResetRequests = func() *resetRequests { 98 go removeOutdated()
78 rr := &resetRequests{reqs: map[string]*timedUser{}} 99 }
79 go func() { 100
80 for { 101 func removeOutdated() {
81 time.Sleep(2 * time.Minute) 102 for {
82 rr.removeOutdated() 103 time.Sleep(cleanupPause)
83 } 104 err := asServiceUser(func(db *sql.DB) error {
84 }() 105 good := time.Now().Add(-passwordResetValid)
85 return rr 106 _, err := db.Exec(cleanupRequestsSQL, good)
86 }() 107 return err
87 108 })
88 func (rr *resetRequests) len() int { 109 if err != nil {
89 rr.Lock() 110 log.Printf("error: %v\n", err)
90 l := len(rr.reqs)
91 rr.Unlock()
92 return l
93 }
94
95 func (rr *resetRequests) userAllowed(user string) bool {
96 rr.Lock()
97 defer rr.Unlock()
98 var count int
99 for _, v := range rr.reqs {
100 if v.user == user {
101 if count++; count >= maxPasswordRequestsPerUser {
102 return false
103 }
104 }
105 }
106 return true
107 }
108
109 func (rr *resetRequests) store(hash, user, email string) {
110 now := time.Now()
111 rr.Lock()
112 rr.reqs[hash] = &timedUser{user, email, now}
113 rr.Unlock()
114 }
115
116 func (rr *resetRequests) fetch(hash string) *timedUser {
117 rr.Lock()
118 defer rr.Unlock()
119 tu := rr.reqs[hash]
120 if tu == nil {
121 return nil
122 }
123 if tu.time.Before(time.Now().Add(-passwordResetValid)) {
124 delete(rr.reqs, hash)
125 return nil
126 }
127 return tu
128 }
129
130 func (rr *resetRequests) delete(hash string) {
131 rr.Lock()
132 delete(rr.reqs, hash)
133 rr.Unlock()
134 }
135
136 func (rr *resetRequests) removeOutdated() {
137 good := time.Now().Add(-passwordResetValid)
138 rr.Lock()
139 defer rr.Unlock()
140 for k, v := range rr.reqs {
141 if v.time.Before(good) {
142 delete(rr.reqs, k)
143 } 111 }
144 } 112 }
145 } 113 }
146 114
147 func requestMessageBody(https, user, hash, server string) string { 115 func requestMessageBody(https, user, hash, server string) string {
187 return "https" 155 return "https"
188 } 156 }
189 return "http" 157 return "http"
190 } 158 }
191 159
192 const (
193 hashLength = 32
194 passwordLength = 20
195 )
196
197 func generateHash() string { 160 func generateHash() string {
198 return hex.EncodeToString(auth.GenerateRandomKey(hashLength)) 161 return hex.EncodeToString(auth.GenerateRandomKey(hashLength))
199 } 162 }
200 163
201 func randomString(n int) string { 164 func randomString(n int) string {
223 // First try pwgen 186 // First try pwgen
224 out, err := exec.Command("pwgen", "-y", "20", "1").Output() 187 out, err := exec.Command("pwgen", "-y", "20", "1").Output()
225 if err == nil { 188 if err == nil {
226 return strings.TrimSpace(string(out)) 189 return strings.TrimSpace(string(out))
227 } 190 }
228
229 // Use internal generator. 191 // Use internal generator.
230 return randomString(20) 192 return randomString(20)
231
232 } 193 }
233 194
234 func sendMail(email, subject, body string) error { 195 func sendMail(email, subject, body string) error {
235 cfg := &config.Config 196 cfg := &config.Config
236 m := gomail.NewMessage() 197 m := gomail.NewMessage()
252 } 213 }
253 214
254 func passwordResetRequest( 215 func passwordResetRequest(
255 input interface{}, 216 input interface{},
256 req *http.Request, 217 req *http.Request,
257 db *sql.DB, 218 _ *sql.DB,
258 ) (jr JSONResult, err error) { 219 ) (jr JSONResult, err error) {
259
260 // Limit total number of password requests.
261 if passwordResetRequests.len() >= maxPasswordResets {
262 err = JSONError{
263 Code: http.StatusServiceUnavailable,
264 Message: "Too much password reset request",
265 }
266 return
267 }
268 220
269 user := input.(*PWResetUser) 221 user := input.(*PWResetUser)
270 222
271 if user.User == "" { 223 if user.User == "" {
272 err = JSONError{http.StatusBadRequest, "Invalid user name"} 224 err = JSONError{http.StatusBadRequest, "Invalid user name"}
273 return 225 return
274 } 226 }
275 227
276 cfg := &config.Config 228 var hash, email string
277 if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { 229
278 return 230 if err = asServiceUser(func(db *sql.DB) error {
279 } 231
280 defer db.Close() 232 var count int64
281 233 if err := db.QueryRow(countRequestsSQL).Scan(&count); err != nil {
282 var email string 234 return err
283 err = db.QueryRow(userExistsSQL, user.User).Scan(&email) 235 }
284 236
285 switch { 237 // Limit total number of password requests.
286 case err == sql.ErrNoRows: 238 if count >= maxPasswordResets {
287 err = JSONError{http.StatusNotFound, "User does not exist."} 239 return JSONError{
288 return 240 Code: http.StatusServiceUnavailable,
289 case err != nil: 241 Message: "Too much password reset request",
290 return 242 }
291 } 243 }
292 244
293 // Limit requests per user 245 err := db.QueryRow(userExistsSQL, user.User).Scan(&email)
294 if !passwordResetRequests.userAllowed(user.User) { 246
295 err = JSONError{ 247 switch {
296 Code: http.StatusServiceUnavailable, 248 case err == sql.ErrNoRows:
297 Message: "Too much password reset requests for user", 249 return JSONError{http.StatusNotFound, "User does not exist."}
298 } 250 case err != nil:
299 return 251 return err
300 } 252 }
301 253
302 hash := generateHash() 254 if err := db.QueryRow(countRequestsUserSQL, user.User).Scan(&count); err != nil {
303 255 return err
304 passwordResetRequests.store(hash, user.User, email) 256 }
305 257
306 body := requestMessageBody(useHTTPS(req), user.User, hash, req.Host) 258 // Limit requests per user
307 259 if count >= maxPasswordRequestsPerUser {
308 if err = sendMail(email, "Password Reset Link", body); err != nil { 260 return JSONError{
309 return 261 Code: http.StatusServiceUnavailable,
310 } 262 Message: "Too much password reset requests for user",
311 263 }
312 jr.Result = &struct { 264 }
313 SendTo string `json:"send-to"` 265
314 }{email} 266 hash = generateHash()
315 267 _, err = db.Exec(insertRequestSQL, hash, user.User)
268 return err
269 }); err == nil {
270 body := requestMessageBody(useHTTPS(req), user.User, hash, req.Host)
271
272 if err = sendMail(email, "Password Reset Link", body); err == nil {
273 jr.Result = &struct {
274 SendTo string `json:"send-to"`
275 }{email}
276 }
277 }
316 return 278 return
317 } 279 }
318 280
319 func passwordReset( 281 func passwordReset(
320 _ interface{}, 282 _ interface{},
321 req *http.Request, 283 req *http.Request,
322 db *sql.DB, 284 _ *sql.DB,
323 ) (jr JSONResult, err error) { 285 ) (jr JSONResult, err error) {
324 286
325 hash := mux.Vars(req)["hash"] 287 hash := mux.Vars(req)["hash"]
326 if _, err = hex.DecodeString(hash); err != nil { 288 if _, err = hex.DecodeString(hash); err != nil {
327 err = JSONError{http.StatusBadRequest, "Invalid hash"} 289 err = JSONError{http.StatusBadRequest, "Invalid hash"}
328 return 290 return
329 } 291 }
330 292
331 tu := passwordResetRequests.fetch(hash) 293 var email, user, password string
332 if tu == nil { 294
333 err = JSONError{http.StatusNotFound, "No such hash"} 295 if err = asServiceUser(func(db *sql.DB) error {
334 return 296 err := db.QueryRow(findRequestSQL, hash).Scan(&email, &user)
335 } 297 switch {
336 298 case err == sql.ErrNoRows:
337 password := generateNewPassword() 299 return JSONError{http.StatusNotFound, "No such hash"}
338 300 case err != nil:
339 cfg := &config.Config 301 return err
340 if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { 302 }
341 return 303 password = generateNewPassword()
342 } 304 res, err := db.Exec(updatePasswordSQL, password, user)
343 defer db.Close() 305 if err != nil {
344 306 return err
345 var res sql.Result 307 }
346 if res, err = db.Exec( 308 if n, err2 := res.RowsAffected(); err2 == nil && n == 0 {
347 updatePasswordSQL, 309 return JSONError{http.StatusNotFound, "User not found"}
348 password, 310 }
349 tu.user, 311 _, err = db.Exec(deleteRequestSQL, hash)
350 ); err != nil { 312 return err
351 return 313 }); err == nil {
352 } 314 body := changedMessageBody(useHTTPS(req), user, password, req.Host)
353 315 if err = sendMail(email, "Password Reset Done", body); err == nil {
354 passwordResetRequests.delete(hash) 316 jr.Result = &struct {
355 317 SendTo string `json:"send-to"`
356 if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { 318 }{email}
357 err = JSONError{http.StatusNotFound, "User not found"} 319 }
358 return 320 }
359 }
360
361 body := changedMessageBody(useHTTPS(req), tu.user, password, req.Host)
362 if err = sendMail(tu.email, "Password Reset Done", body); err != nil {
363 return
364 }
365
366 jr.Result = &struct {
367 Message string `json:"message"`
368 }{"User password changed"}
369
370 return 321 return
371 } 322 }