aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/ratelimit
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-13 23:31:19 +0900
committernsfisis <nsfisis@gmail.com>2026-02-13 23:31:19 +0900
commit7757f26295cbf19c4d6fa068e2cb6bdc2589d01a (patch)
tree48d1145bacad99018378f20aa9826b04e7fa2832 /backend/ratelimit
parent470b7235b80d082009ad350e2b33ef6637209e02 (diff)
downloadphperkaigi-2026-albatross-7757f26295cbf19c4d6fa068e2cb6bdc2589d01a.tar.gz
phperkaigi-2026-albatross-7757f26295cbf19c4d6fa068e2cb6bdc2589d01a.tar.zst
phperkaigi-2026-albatross-7757f26295cbf19c4d6fa068e2cb6bdc2589d01a.zip
feat(auth): add login rate limiting per IP
Prevent brute-force attacks by limiting POST /login to 5 requests per minute per IP address using golang.org/x/time/rate. Unused entries are cleaned up after 10 minutes of inactivity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/ratelimit')
-rw-r--r--backend/ratelimit/ratelimit.go73
1 files changed, 73 insertions, 0 deletions
diff --git a/backend/ratelimit/ratelimit.go b/backend/ratelimit/ratelimit.go
new file mode 100644
index 0000000..3200907
--- /dev/null
+++ b/backend/ratelimit/ratelimit.go
@@ -0,0 +1,73 @@
+package ratelimit
+
+import (
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "golang.org/x/time/rate"
+)
+
+type entry struct {
+ limiter *rate.Limiter
+ lastSeen time.Time
+}
+
+type IPRateLimiter struct {
+ entries sync.Map
+ rate rate.Limit
+ burst int
+}
+
+func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
+ rl := &IPRateLimiter{
+ rate: r,
+ burst: burst,
+ }
+ go rl.cleanup()
+ return rl
+}
+
+func (rl *IPRateLimiter) getLimiter(ip string) *rate.Limiter {
+ now := time.Now()
+ if v, ok := rl.entries.Load(ip); ok {
+ e := v.(*entry)
+ e.lastSeen = now
+ return e.limiter
+ }
+ limiter := rate.NewLimiter(rl.rate, rl.burst)
+ rl.entries.Store(ip, &entry{limiter: limiter, lastSeen: now})
+ return limiter
+}
+
+func (rl *IPRateLimiter) cleanup() {
+ for {
+ time.Sleep(10 * time.Minute)
+ rl.entries.Range(func(key, value any) bool {
+ e := value.(*entry)
+ if time.Since(e.lastSeen) > 10*time.Minute {
+ rl.entries.Delete(key)
+ }
+ return true
+ })
+ }
+}
+
+func LoginRateLimitMiddleware(rl *IPRateLimiter) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ if c.Request().Method != http.MethodPost || !strings.HasSuffix(c.Path(), "/login") {
+ return next(c)
+ }
+ ip := c.RealIP()
+ if !rl.getLimiter(ip).Allow() {
+ return c.JSON(http.StatusTooManyRequests, map[string]string{
+ "message": "Too many login attempts. Please try again later.",
+ })
+ }
+ return next(c)
+ }
+ }
+}