diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index a5d5720..e7bd43c 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,11 +3,13 @@ package auth import ( "errors" "net/http" + "time" _ "embed" "dynatron.me/x/stillbox/pkg/config" "github.com/go-chi/chi/v5" + "github.com/go-chi/httprate" "github.com/go-chi/jwtauth/v5" ) @@ -30,6 +32,7 @@ type Authenticator interface { } type Auth struct { + rl *httprate.RateLimiter jwt *jwtauth.JWTAuth cfg config.Auth } @@ -37,6 +40,7 @@ type Auth struct { // NewAuthenticator creates a new Authenticator with the provided config. func NewAuthenticator(cfg config.Auth) *Auth { a := &Auth{ + rl: httprate.NewRateLimiter(5, time.Minute), cfg: cfg, } a.initJWT() diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index b2eed4c..4dc8bf6 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -225,6 +225,10 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) { return } + if a.rl.RespondOnLimit(w, r, creds.Username) { + return + } + if creds.Username == "" || creds.Password == "" { http.Error(w, "blank credentials", http.StatusBadRequest) return diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 5856bdb..a7f0788 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -10,10 +10,7 @@ import ( "dynatron.me/x/stillbox/client" "dynatron.me/x/stillbox/internal/version" "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/middleware" "github.com/go-chi/httprate" "github.com/go-chi/render" ) @@ -29,8 +26,7 @@ func (s *Server) setupRoutes() { } r := s.r - r.Use(middleware.WithValue(database.DBCtxKey, s.db)) - r.Use(middleware.WithValue(tgstore.StoreCtxKey, s.tgs)) + r.Use(s.WithCtxStores()) s.installPprof() @@ -47,10 +43,15 @@ func (s *Server) setupRoutes() { s.rateLimit(r) r.Use(render.SetContentType(render.ContentTypeJSON)) // public routes - s.auth.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) { s.rateLimit(r) 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) { if s.conf.RateLimit.Verify() { r.Use(rateLimiter(&s.conf.RateLimit)) } } + func rateLimiter(cfg *config.RateLimit) func(http.Handler) http.Handler { return httprate.LimitByRealIP(cfg.Requests, cfg.Over) }