diff --git a/veza-backend-api/internal/config/middlewares_init.go b/veza-backend-api/internal/config/middlewares_init.go index 87fa14917..29dc4c840 100644 --- a/veza-backend-api/internal/config/middlewares_init.go +++ b/veza-backend-api/internal/config/middlewares_init.go @@ -75,6 +75,13 @@ func (c *Config) initMiddlewares() error { c.AuthMiddleware.SetPresenceService(c.PresenceService) } + // BE-SVC-002: Wire per-user rate limiter into the auth middleware so it + // fires automatically after every successful RequireAuth on any route. + // Previously UserRateLimiter was created but never mounted (dead wiring). + if c.UserRateLimiter != nil { + c.AuthMiddleware.SetUserRateLimiter(c.UserRateLimiter) + } + return nil } diff --git a/veza-backend-api/internal/middleware/auth.go b/veza-backend-api/internal/middleware/auth.go index a0ee70294..0da984dc4 100644 --- a/veza-backend-api/internal/middleware/auth.go +++ b/veza-backend-api/internal/middleware/auth.go @@ -64,6 +64,7 @@ type AuthMiddleware struct { presenceService PresenceUpdater // v0.301: Optional, updates last_seen_at on auth tokenBlacklist TokenBlacklistChecker // VEZA-SEC-006: Optional, nil if Redis unavailable twoFactorChecker TwoFactorChecker // SFIX-001: Optional, for MFA enforcement + userRateLimiter *UserRateLimiter // BE-SVC-002: Optional, per-user rate limiting applied post-auth logger *zap.Logger } @@ -102,6 +103,15 @@ func (am *AuthMiddleware) SetTwoFactorChecker(tfc TwoFactorChecker) { am.twoFactorChecker = tfc } +// SetUserRateLimiter wires the per-user rate limiter so it runs automatically +// after every successful RequireAuth / RequireAuthWithMFA call. Centralising it +// here avoids sprinkling UserRateLimiter.Middleware() across every protected +// route group. nil is fine — limiter simply skipped when absent. +// (BE-SVC-002) +func (am *AuthMiddleware) SetUserRateLimiter(url *UserRateLimiter) { + am.userRateLimiter = url +} + // isSessionCheckRequest returns true for GET /auth/me (or path ending with /auth/me). // Used to avoid WARN logs when the frontend probes session without a token (expected case). func isSessionCheckRequest(path string) bool { @@ -335,15 +345,30 @@ func (am *AuthMiddleware) authenticate(c *gin.Context) (uuid.UUID, bool) { // RequireAuth middleware qui exige une authentification func (am *AuthMiddleware) RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { - if userID, ok := am.authenticate(c); ok { - // v0.301 Lot P1: Update presence (last_seen_at) on each authenticated request - if am.presenceService != nil { - go func() { - _ = am.presenceService.UpdatePresence(context.Background(), userID, "online") - }() - } - c.Next() + userID, ok := am.authenticate(c) + if !ok { + return } + + // v0.301 Lot P1: Update presence (last_seen_at) on each authenticated request + if am.presenceService != nil { + go func() { + _ = am.presenceService.UpdatePresence(context.Background(), userID, "online") + }() + } + + // BE-SVC-002: Per-user rate limiting runs after auth so user_id is set. + // Limiter writes 429 + X-RateLimit-* headers and aborts the chain if + // the user exceeds their window; c.Next() below only fires when + // the limiter lets the request through. + if am.userRateLimiter != nil { + am.userRateLimiter.Middleware()(c) + if c.IsAborted() { + return + } + } + + c.Next() } }