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:
2026-08-22 11:47:42 -07:00
parent 96b0ce795a
commit 59513ab75e
9 changed files with 287 additions and 50 deletions
+30 -15
View File
@@ -24,17 +24,24 @@ import (
)
type Config struct {
AdminUsername string
SecureCookie bool
Blob blob.Uploader
// AdminSetupSecret, when set, can promote the first registrant who also
// posts the matching setup_secret. It is ignored once any admin exists.
AdminSetupSecret string
SecureCookie bool
// TrustProxy enables X-Forwarded-For / RealIP only behind a known proxy.
TrustProxy bool
Blob blob.Uploader
}
type Server struct {
store store.Store
sessions *scs.SessionManager
tmpl *template.Template
cfg Config
static http.Handler
store store.Store
sessions *scs.SessionManager
tmpl *template.Template
cfg Config
static http.Handler
loginIP *throttle
loginUser *throttle
registerIP *throttle
}
type page struct {
@@ -124,18 +131,23 @@ func New(st store.Store, sessionStore scs.Store, templateFS fs.FS, staticFS fs.F
}
return &Server{
store: st,
sessions: sessions,
tmpl: tmpl,
cfg: cfg,
static: http.StripPrefix("/static/", http.FileServer(http.FS(sub))),
store: st,
sessions: sessions,
tmpl: tmpl,
cfg: cfg,
static: http.StripPrefix("/static/", http.FileServer(http.FS(sub))),
loginIP: newThrottle(20, 15*time.Minute),
loginUser: newThrottle(10, 15*time.Minute),
registerIP: newThrottle(10, 15*time.Minute),
}, nil
}
func (s *Server) Handler() http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
if s.cfg.TrustProxy {
r.Use(middleware.RealIP)
}
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(func(next http.Handler) http.Handler {
@@ -507,7 +519,10 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if !s.requireCSRF(w, r) {
return
}
s.sessions.Remove(r.Context(), "user_id")
if err := s.sessions.Destroy(r.Context()); err != nil {
http.Error(w, "could not sign out", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}