A community based topic aggregation platform built on atproto
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)