Rate limit login keyed by username

This commit is contained in:
Daniel Ponte 2025-01-06 08:22:51 -05:00
parent a5fc6825b1
commit 108c6ec62f
3 changed files with 27 additions and 6 deletions

View file

@ -3,11 +3,13 @@ package auth
import ( import (
"errors" "errors"
"net/http" "net/http"
"time"
_ "embed" _ "embed"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/httprate"
"github.com/go-chi/jwtauth/v5" "github.com/go-chi/jwtauth/v5"
) )
@ -30,6 +32,7 @@ type Authenticator interface {
} }
type Auth struct { type Auth struct {
rl *httprate.RateLimiter
jwt *jwtauth.JWTAuth jwt *jwtauth.JWTAuth
cfg config.Auth cfg config.Auth
} }
@ -37,6 +40,7 @@ type Auth struct {
// NewAuthenticator creates a new Authenticator with the provided config. // NewAuthenticator creates a new Authenticator with the provided config.
func NewAuthenticator(cfg config.Auth) *Auth { func NewAuthenticator(cfg config.Auth) *Auth {
a := &Auth{ a := &Auth{
rl: httprate.NewRateLimiter(5, time.Minute),
cfg: cfg, cfg: cfg,
} }
a.initJWT() a.initJWT()

View file

@ -225,6 +225,10 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) {
return return
} }
if a.rl.RespondOnLimit(w, r, creds.Username) {
return
}
if creds.Username == "" || creds.Password == "" { if creds.Username == "" || creds.Password == "" {
http.Error(w, "blank credentials", http.StatusBadRequest) http.Error(w, "blank credentials", http.StatusBadRequest)
return return

View file

@ -10,10 +10,7 @@ import (
"dynatron.me/x/stillbox/client" "dynatron.me/x/stillbox/client"
"dynatron.me/x/stillbox/internal/version" "dynatron.me/x/stillbox/internal/version"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate" "github.com/go-chi/httprate"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
@ -29,8 +26,7 @@ func (s *Server) setupRoutes() {
} }
r := s.r r := s.r
r.Use(middleware.WithValue(database.DBCtxKey, s.db)) r.Use(s.WithCtxStores())
r.Use(middleware.WithValue(tgstore.StoreCtxKey, s.tgs))
s.installPprof() s.installPprof()
@ -47,10 +43,15 @@ func (s *Server) setupRoutes() {
s.rateLimit(r) s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(render.SetContentType(render.ContentTypeJSON))
// public routes // public routes
s.auth.PublicRoutes(r)
s.sources.PublicRoutes(r) s.sources.PublicRoutes(r)
}) })
r.Group(func(r chi.Router) {
// auth routes get rate-limited heavily, but not using middleware
r.Use(render.SetContentType(render.ContentTypeJSON))
s.auth.PublicRoutes(r)
})
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
s.rateLimit(r) s.rateLimit(r)
r.Use(s.auth.VerifyMiddleware()) r.Use(s.auth.VerifyMiddleware())
@ -61,11 +62,23 @@ func (s *Server) setupRoutes() {
}) })
} }
// WithCtxStores is a middleware that installs all stores in the request context.
func (s *Server) WithCtxStores() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(s.addStoresTo(r.Context()))
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func (s *Server) rateLimit(r chi.Router) { func (s *Server) rateLimit(r chi.Router) {
if s.conf.RateLimit.Verify() { if s.conf.RateLimit.Verify() {
r.Use(rateLimiter(&s.conf.RateLimit)) r.Use(rateLimiter(&s.conf.RateLimit))
} }
} }
func rateLimiter(cfg *config.RateLimit) func(http.Handler) http.Handler { func rateLimiter(cfg *config.RateLimit) func(http.Handler) http.Handler {
return httprate.LimitByRealIP(cfg.Requests, cfg.Over) return httprate.LimitByRealIP(cfg.Requests, cfg.Over)
} }