package middleware import ( "net/http" "time" "github.com/go-chi/httprate" ) const ( // loginRateLimit is the maximum number of login attempts // per interval. loginRateLimit = 5 // loginRateInterval is the time window for the rate limit. loginRateInterval = 1 * time.Minute // passwordChangeRateLimit is the maximum number of password // change attempts per interval. Each attempt verifies the // current password, so the endpoint must be rate-limited // like any other password-based authentication endpoint. passwordChangeRateLimit = 5 // passwordChangeRateInterval is the time window for the // password change rate limit. passwordChangeRateInterval = 1 * time.Minute ) // LoginRateLimit returns middleware that enforces per-IP rate // limiting on login attempts using go-chi/httprate. Only POST // requests are rate-limited; GET requests (rendering the login // form) pass through unaffected. When the rate limit is exceeded, // a 429 Too Many Requests response is returned. IP extraction // honours X-Forwarded-For, X-Real-IP, and True-Client-IP headers // for reverse-proxy setups. func (m *Middleware) LoginRateLimit() func(http.Handler) http.Handler { return m.postRateLimit( loginRateLimit, loginRateInterval, "login rate limit exceeded", "Too many login attempts. Please try again later.", ) } // PasswordChangeRateLimit returns middleware that enforces // per-IP rate limiting on password change attempts. The change // endpoint verifies the current password, so without a limit a // stolen session could be used to brute-force it; the limit // matches the login endpoint's. func (m *Middleware) PasswordChangeRateLimit() func(http.Handler) http.Handler { return m.postRateLimit( passwordChangeRateLimit, passwordChangeRateInterval, "password change rate limit exceeded", "Too many password change attempts. "+ "Please try again later.", ) } // postRateLimit builds middleware that enforces a per-IP rate // limit on POST requests only; all other methods pass through // unaffected. Requests over the limit receive a 429 with the // given response message, and each rejection is logged with the // given log message. IP extraction honours X-Forwarded-For, // X-Real-IP, and True-Client-IP headers for reverse-proxy // setups. func (m *Middleware) postRateLimit( limit int, interval time.Duration, logMessage, responseMessage string, ) func(http.Handler) http.Handler { limiter := httprate.Limit( limit, interval, httprate.WithKeyFuncs(httprate.KeyByRealIP), httprate.WithLimitHandler(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { m.log.Warn(logMessage, "path", r.URL.Path, ) http.Error( w, responseMessage, http.StatusTooManyRequests, ) }, )), ) return func(next http.Handler) http.Handler { limited := limiter(next) return http.HandlerFunc(func( w http.ResponseWriter, r *http.Request, ) { // Only rate-limit POST requests. if r.Method != http.MethodPost { next.ServeHTTP(w, r) return } limited.ServeHTTP(w, r) }) } }