Harden auth: setup secret, throttling, session destroy, secure cookies.
Replace username-based admin bootstrap with a one-time setup secret, rate-limit login/register, equalize login bcrypt timing, cap passwords at 72 bytes, destroy sessions on logout, and require Secure cookies when PORT is set.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// throttle is a simple sliding-window rate limiter for auth endpoints.
|
||||
type throttle struct {
|
||||
mu sync.Mutex
|
||||
hits map[string][]time.Time
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
func newThrottle(limit int, window time.Duration) *throttle {
|
||||
return &throttle{
|
||||
hits: map[string][]time.Time{},
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *throttle) allow(key string) bool {
|
||||
if t == nil || key == "" {
|
||||
return true
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-t.window)
|
||||
xs := t.hits[key]
|
||||
n := 0
|
||||
for _, ts := range xs {
|
||||
if ts.After(cutoff) {
|
||||
xs[n] = ts
|
||||
n++
|
||||
}
|
||||
}
|
||||
xs = xs[:n]
|
||||
if len(xs) >= t.limit {
|
||||
t.hits[key] = xs
|
||||
return false
|
||||
}
|
||||
t.hits[key] = append(xs, now)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) clientIP(r *http.Request) string {
|
||||
if s.cfg.TrustProxy {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
return strings.TrimSpace(strings.Split(xff, ",")[0])
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func authTooMany(w http.ResponseWriter) {
|
||||
http.Error(w, "too many attempts; try again later", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
func (s *Server) allowLoginAttempt(w http.ResponseWriter, r *http.Request, usernameKey string) bool {
|
||||
if !s.loginIP.allow(s.clientIP(r)) || !s.loginUser.allow(usernameKey) {
|
||||
authTooMany(w)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) allowRegisterAttempt(w http.ResponseWriter, r *http.Request) bool {
|
||||
if !s.registerIP.allow(s.clientIP(r)) {
|
||||
authTooMany(w)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user