A community based topic aggregation platform built on atproto
at main 10 kB view raw
1package e2e 2 3import ( 4 "Coves/internal/api/middleware" 5 "net/http" 6 "net/http/httptest" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11) 12 13// TestRateLimiting_E2E_OAuthEndpoints tests OAuth-specific rate limiting 14// OAuth endpoints have stricter rate limits to prevent: 15// - Credential stuffing attacks on login endpoints (10 req/min) 16// - OAuth state exhaustion 17// - Refresh token abuse (20 req/min) 18func TestRateLimiting_E2E_OAuthEndpoints(t *testing.T) { 19 t.Run("Login endpoints have 10 req/min limit", func(t *testing.T) { 20 // Create rate limiter matching oauth.go config: 10 requests per minute 21 loginLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 22 23 // Mock OAuth login handler 24 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 w.WriteHeader(http.StatusOK) 26 _, _ = w.Write([]byte("OK")) 27 }) 28 29 handler := loginLimiter.Middleware(testHandler) 30 clientIP := "192.168.1.200:12345" 31 32 // Make exactly 10 requests (at limit) 33 for i := 0; i < 10; i++ { 34 req := httptest.NewRequest("GET", "/oauth/login", nil) 35 req.RemoteAddr = clientIP 36 rr := httptest.NewRecorder() 37 38 handler.ServeHTTP(rr, req) 39 40 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 41 } 42 43 // 11th request should be rate limited 44 req := httptest.NewRequest("GET", "/oauth/login", nil) 45 req.RemoteAddr = clientIP 46 rr := httptest.NewRecorder() 47 48 handler.ServeHTTP(rr, req) 49 50 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Request 11 should be rate limited") 51 assert.Contains(t, rr.Body.String(), "Rate limit exceeded", "Should have rate limit error message") 52 }) 53 54 t.Run("Mobile login endpoints have 10 req/min limit", func(t *testing.T) { 55 loginLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 56 57 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 w.WriteHeader(http.StatusOK) 59 }) 60 61 handler := loginLimiter.Middleware(testHandler) 62 clientIP := "192.168.1.201:12345" 63 64 // Make 10 requests 65 for i := 0; i < 10; i++ { 66 req := httptest.NewRequest("GET", "/oauth/mobile/login", nil) 67 req.RemoteAddr = clientIP 68 rr := httptest.NewRecorder() 69 handler.ServeHTTP(rr, req) 70 assert.Equal(t, http.StatusOK, rr.Code) 71 } 72 73 // 11th request blocked 74 req := httptest.NewRequest("GET", "/oauth/mobile/login", nil) 75 req.RemoteAddr = clientIP 76 rr := httptest.NewRecorder() 77 handler.ServeHTTP(rr, req) 78 79 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Mobile login should be rate limited at 10 req/min") 80 }) 81 82 t.Run("Refresh endpoint has 20 req/min limit", func(t *testing.T) { 83 // Refresh has higher limit (20 req/min) for legitimate token refresh 84 refreshLimiter := middleware.NewRateLimiter(20, 1*time.Minute) 85 86 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 w.WriteHeader(http.StatusOK) 88 }) 89 90 handler := refreshLimiter.Middleware(testHandler) 91 clientIP := "192.168.1.202:12345" 92 93 // Make 20 requests 94 for i := 0; i < 20; i++ { 95 req := httptest.NewRequest("POST", "/oauth/refresh", nil) 96 req.RemoteAddr = clientIP 97 rr := httptest.NewRecorder() 98 handler.ServeHTTP(rr, req) 99 assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1) 100 } 101 102 // 21st request blocked 103 req := httptest.NewRequest("POST", "/oauth/refresh", nil) 104 req.RemoteAddr = clientIP 105 rr := httptest.NewRecorder() 106 handler.ServeHTTP(rr, req) 107 108 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Refresh should be rate limited at 20 req/min") 109 }) 110 111 t.Run("Logout endpoint has 10 req/min limit", func(t *testing.T) { 112 logoutLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 113 114 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 115 w.WriteHeader(http.StatusOK) 116 }) 117 118 handler := logoutLimiter.Middleware(testHandler) 119 clientIP := "192.168.1.203:12345" 120 121 // Make 10 requests 122 for i := 0; i < 10; i++ { 123 req := httptest.NewRequest("POST", "/oauth/logout", nil) 124 req.RemoteAddr = clientIP 125 rr := httptest.NewRecorder() 126 handler.ServeHTTP(rr, req) 127 assert.Equal(t, http.StatusOK, rr.Code) 128 } 129 130 // 11th request blocked 131 req := httptest.NewRequest("POST", "/oauth/logout", nil) 132 req.RemoteAddr = clientIP 133 rr := httptest.NewRecorder() 134 handler.ServeHTTP(rr, req) 135 136 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Logout should be rate limited at 10 req/min") 137 }) 138 139 t.Run("OAuth callback has 10 req/min limit", func(t *testing.T) { 140 // Callback uses same limiter as login (part of auth flow) 141 callbackLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 142 143 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 w.WriteHeader(http.StatusOK) 145 }) 146 147 handler := callbackLimiter.Middleware(testHandler) 148 clientIP := "192.168.1.204:12345" 149 150 // Make 10 requests 151 for i := 0; i < 10; i++ { 152 req := httptest.NewRequest("GET", "/oauth/callback", nil) 153 req.RemoteAddr = clientIP 154 rr := httptest.NewRecorder() 155 handler.ServeHTTP(rr, req) 156 assert.Equal(t, http.StatusOK, rr.Code) 157 } 158 159 // 11th request blocked 160 req := httptest.NewRequest("GET", "/oauth/callback", nil) 161 req.RemoteAddr = clientIP 162 rr := httptest.NewRecorder() 163 handler.ServeHTTP(rr, req) 164 165 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Callback should be rate limited at 10 req/min") 166 }) 167 168 t.Run("OAuth rate limits are stricter than global limit", func(t *testing.T) { 169 // Verify OAuth limits are more restrictive than global 100 req/min 170 const globalLimit = 100 171 const oauthLoginLimit = 10 172 const oauthRefreshLimit = 20 173 174 assert.Less(t, oauthLoginLimit, globalLimit, "OAuth login limit should be stricter than global") 175 assert.Less(t, oauthRefreshLimit, globalLimit, "OAuth refresh limit should be stricter than global") 176 assert.Greater(t, oauthRefreshLimit, oauthLoginLimit, "Refresh limit should be higher than login (legitimate use case)") 177 }) 178 179 t.Run("OAuth limits prevent credential stuffing", func(t *testing.T) { 180 // Simulate credential stuffing attack 181 loginLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 182 183 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 184 // Simulate failed login attempts 185 w.WriteHeader(http.StatusUnauthorized) 186 }) 187 188 handler := loginLimiter.Middleware(testHandler) 189 attackerIP := "203.0.113.50:12345" 190 191 // Attacker tries 15 login attempts (credential stuffing) 192 successfulAttempts := 0 193 blockedAttempts := 0 194 195 for i := 0; i < 15; i++ { 196 req := httptest.NewRequest("GET", "/oauth/login", nil) 197 req.RemoteAddr = attackerIP 198 rr := httptest.NewRecorder() 199 200 handler.ServeHTTP(rr, req) 201 202 if rr.Code == http.StatusUnauthorized { 203 successfulAttempts++ // Reached handler (even if auth failed) 204 } else if rr.Code == http.StatusTooManyRequests { 205 blockedAttempts++ 206 } 207 } 208 209 // Rate limiter should block 5 attempts after first 10 210 assert.Equal(t, 10, successfulAttempts, "Should allow 10 login attempts") 211 assert.Equal(t, 5, blockedAttempts, "Should block 5 attempts after limit reached") 212 }) 213 214 t.Run("OAuth limits are per-endpoint", func(t *testing.T) { 215 // Each endpoint gets its own rate limiter 216 // This test verifies that limits are independent per endpoint 217 loginLimiter := middleware.NewRateLimiter(10, 1*time.Minute) 218 refreshLimiter := middleware.NewRateLimiter(20, 1*time.Minute) 219 220 loginHandler := loginLimiter.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 221 w.WriteHeader(http.StatusOK) 222 })) 223 224 refreshHandler := refreshLimiter.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 w.WriteHeader(http.StatusOK) 226 })) 227 228 clientIP := "192.168.1.205:12345" 229 230 // Exhaust login limit 231 for i := 0; i < 10; i++ { 232 req := httptest.NewRequest("GET", "/oauth/login", nil) 233 req.RemoteAddr = clientIP 234 rr := httptest.NewRecorder() 235 loginHandler.ServeHTTP(rr, req) 236 assert.Equal(t, http.StatusOK, rr.Code) 237 } 238 239 // Login limit exhausted 240 req := httptest.NewRequest("GET", "/oauth/login", nil) 241 req.RemoteAddr = clientIP 242 rr := httptest.NewRecorder() 243 loginHandler.ServeHTTP(rr, req) 244 assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Login should be rate limited") 245 246 // Refresh endpoint should still work (independent limiter) 247 req = httptest.NewRequest("POST", "/oauth/refresh", nil) 248 req.RemoteAddr = clientIP 249 rr = httptest.NewRecorder() 250 refreshHandler.ServeHTTP(rr, req) 251 assert.Equal(t, http.StatusOK, rr.Code, "Refresh should not be affected by login rate limit") 252 }) 253} 254 255// OAuth Rate Limiting Configuration Documentation 256// ================================================ 257// This test file validates OAuth-specific rate limits applied in oauth.go: 258// 259// 1. Login Endpoints (Credential Stuffing Protection) 260// - Endpoints: /oauth/login, /oauth/mobile/login, /oauth/callback 261// - Limit: 10 requests per minute per IP 262// - Reason: Prevent brute force and credential stuffing attacks 263// - Implementation: internal/api/routes/oauth.go:21 264// 265// 2. Refresh Endpoint (Token Refresh) 266// - Endpoint: /oauth/refresh 267// - Limit: 20 requests per minute per IP 268// - Reason: Allow legitimate token refresh while preventing abuse 269// - Implementation: internal/api/routes/oauth.go:24 270// 271// 3. Logout Endpoint 272// - Endpoint: /oauth/logout 273// - Limit: 10 requests per minute per IP 274// - Reason: Prevent session exhaustion attacks 275// - Implementation: internal/api/routes/oauth.go:27 276// 277// 4. Metadata Endpoints (No Extra Limit) 278// - Endpoints: /oauth/client-metadata.json, /oauth/jwks.json 279// - Limit: Global 100 requests per minute (from main.go) 280// - Reason: Public metadata, not sensitive to rate abuse 281// 282// Security Benefits: 283// - Credential Stuffing: Limits password guessing to 10 attempts/min 284// - State Exhaustion: Prevents OAuth state generation spam 285// - Token Abuse: Limits refresh token usage while allowing legitimate refresh 286// 287// Rate Limit Hierarchy: 288// - OAuth login: 10 req/min (most restrictive) 289// - OAuth refresh: 20 req/min (moderate) 290// - Comments: 20 req/min (expensive queries) 291// - Global: 100 req/min (baseline)