Add rules language support and improve password validation across the app
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -25,8 +26,21 @@ const sessionDuration = 24 * time.Hour * 14
|
||||
const maxUsernameLen = 64
|
||||
const maxDisplayNameLen = 128
|
||||
const maxPasswordLen = 256
|
||||
const minPasswordLen = 8
|
||||
const maxFieldLen = 2000
|
||||
|
||||
// validatePassword returns "" if the password meets the policy, or a short
|
||||
// error code suitable for the JSON `error` field if it doesn't.
|
||||
func validatePassword(p string) string {
|
||||
if len(p) < minPasswordLen {
|
||||
return "password_too_short"
|
||||
}
|
||||
if len(p) > maxPasswordLen {
|
||||
return "too_long"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func registerAuthRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /api/login", loginRateLimit(handleLogin))
|
||||
mux.HandleFunc("POST /api/logout", handleLogout)
|
||||
@@ -87,7 +101,9 @@ func clientIP(r *http.Request) string {
|
||||
return host
|
||||
}
|
||||
|
||||
func loginAllowed(ip string) bool {
|
||||
// loginStatus returns (remaining, retryAfterSeconds). When remaining == 0 the
|
||||
// caller must reject the request and use retryAfterSeconds for headers.
|
||||
func loginStatus(ip string) (int, int) {
|
||||
loginMu.Lock()
|
||||
defer loginMu.Unlock()
|
||||
a, ok := loginAttempts_[ip]
|
||||
@@ -103,7 +119,20 @@ func loginAllowed(ip string) bool {
|
||||
}
|
||||
}
|
||||
a.times = kept
|
||||
return len(a.times) < loginMaxAttempts
|
||||
remaining := loginMaxAttempts - len(a.times)
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
retryAfter := 0
|
||||
if remaining == 0 && len(a.times) > 0 {
|
||||
// Time until the oldest attempt falls out of the window.
|
||||
next := a.times[0].Add(loginWindow).Sub(time.Now())
|
||||
retryAfter = int(next.Seconds()) + 1
|
||||
if retryAfter < 1 {
|
||||
retryAfter = 1
|
||||
}
|
||||
}
|
||||
return remaining, retryAfter
|
||||
}
|
||||
|
||||
func loginRecord(ip string) {
|
||||
@@ -120,7 +149,12 @@ func loginRecord(ip string) {
|
||||
func loginRateLimit(h http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := clientIP(r)
|
||||
if !loginAllowed(ip) {
|
||||
remaining, retryAfter := loginStatus(ip)
|
||||
w.Header().Set("X-RateLimit-Limit", strconv.Itoa(loginMaxAttempts))
|
||||
w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
|
||||
if remaining == 0 {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
|
||||
w.Header().Set("X-RateLimit-Reset", strconv.Itoa(retryAfter))
|
||||
writeError(w, http.StatusTooManyRequests, "too_many_attempts")
|
||||
return
|
||||
}
|
||||
@@ -245,8 +279,8 @@ func handleUpdateMe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
if len(*req.Password) > maxPasswordLen {
|
||||
writeError(w, http.StatusBadRequest, "too_long")
|
||||
if msg := validatePassword(*req.Password); msg != "" {
|
||||
writeError(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||||
|
||||
Reference in New Issue
Block a user