anderson-ufrj commited on
Commit
eccaf5b
·
1 Parent(s): f70869e

feat(security): enhance CORS configuration for Vercel frontend

Browse files

- Create enhanced CORS middleware with dynamic origin validation
- Add support for Vercel preview deployment patterns
- Configure proper credentials and exposed headers
- Implement CORS validator utility for testing
- Add comprehensive CORS documentation
- Update config with additional Vercel domains
- Support wildcard patterns for deployment previews

docs/CORS_CONFIGURATION.md ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CORS Configuration Guide
2
+
3
+ ## Overview
4
+
5
+ The Cidadão.AI backend uses an enhanced CORS (Cross-Origin Resource Sharing) middleware specifically optimized for integration with Vercel-deployed frontends and other modern deployment platforms.
6
+
7
+ ## Features
8
+
9
+ - ✅ **Dynamic Origin Validation**: Supports wildcard patterns for Vercel preview deployments
10
+ - ✅ **Credentials Support**: Full support for cookies and authentication tokens
11
+ - ✅ **Preflight Optimization**: Efficient handling of OPTIONS requests
12
+ - ✅ **Environment Awareness**: Different rules for development/production
13
+ - ✅ **Custom Headers**: Support for rate limiting and correlation headers
14
+
15
+ ## Configuration
16
+
17
+ ### Environment Variables
18
+
19
+ Configure CORS through environment variables or `.env` file:
20
+
21
+ ```bash
22
+ # Allowed origins (comma-separated)
23
+ CORS_ORIGINS=["https://cidadao-ai-frontend.vercel.app","https://*.vercel.app"]
24
+
25
+ # Allow credentials (cookies, auth headers)
26
+ CORS_ALLOW_CREDENTIALS=true
27
+
28
+ # Allowed methods
29
+ CORS_ALLOW_METHODS=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD"]
30
+
31
+ # Max age for preflight cache (seconds)
32
+ CORS_MAX_AGE=86400
33
+ ```
34
+
35
+ ### Default Configuration
36
+
37
+ The default configuration in `src/core/config.py` includes:
38
+
39
+ ```python
40
+ cors_origins = [
41
+ "http://localhost:3000", # Local development
42
+ "http://localhost:3001", # Alternative port
43
+ "http://127.0.0.1:3000", # IP-based localhost
44
+ "https://cidadao-ai-frontend.vercel.app", # Production
45
+ "https://cidadao-ai.vercel.app", # Alternative production
46
+ "https://*.vercel.app", # Vercel previews
47
+ "https://*.hf.space" # HuggingFace Spaces
48
+ ]
49
+ ```
50
+
51
+ ## Vercel Integration
52
+
53
+ ### Preview Deployments
54
+
55
+ The enhanced CORS middleware automatically allows Vercel preview deployments matching these patterns:
56
+
57
+ - `https://cidadao-ai-frontend-[hash]-[team].vercel.app`
58
+ - `https://cidadao-ai-[hash]-[team].vercel.app`
59
+ - `https://[project]-[hash]-[team].vercel.app`
60
+
61
+ ### Production Setup
62
+
63
+ For production Vercel deployments:
64
+
65
+ 1. Add your production domain to `CORS_ORIGINS`
66
+ 2. Ensure `CORS_ALLOW_CREDENTIALS=true` for authentication
67
+ 3. Configure exposed headers for client access
68
+
69
+ ## Testing CORS
70
+
71
+ ### Using the Validator
72
+
73
+ Run the CORS validator to test your configuration:
74
+
75
+ ```bash
76
+ python -m src.utils.cors_validator
77
+ ```
78
+
79
+ This will:
80
+ - Test all configured origins
81
+ - Validate credentials flow
82
+ - Generate nginx/CloudFlare configurations
83
+
84
+ ### Manual Testing
85
+
86
+ Test CORS with curl:
87
+
88
+ ```bash
89
+ # Preflight request
90
+ curl -X OPTIONS \
91
+ -H "Origin: https://cidadao-ai-frontend.vercel.app" \
92
+ -H "Access-Control-Request-Method: POST" \
93
+ -H "Access-Control-Request-Headers: Content-Type,Authorization" \
94
+ http://localhost:8000/api/v1/chat/message
95
+
96
+ # Actual request
97
+ curl -X POST \
98
+ -H "Origin: https://cidadao-ai-frontend.vercel.app" \
99
+ -H "Content-Type: application/json" \
100
+ -d '{"message":"test"}' \
101
+ http://localhost:8000/api/v1/chat/message
102
+ ```
103
+
104
+ ## Common Issues
105
+
106
+ ### 1. "CORS policy blocked" Error
107
+
108
+ **Symptoms**: Browser shows CORS error, requests fail
109
+
110
+ **Solutions**:
111
+ - Verify origin is in allowed list
112
+ - Check for typos in origin URL
113
+ - Ensure protocol matches (http vs https)
114
+
115
+ ### 2. Credentials Not Working
116
+
117
+ **Symptoms**: Cookies not being set, auth failing
118
+
119
+ **Solutions**:
120
+ - Ensure `CORS_ALLOW_CREDENTIALS=true`
121
+ - Frontend must include `credentials: 'include'` in fetch
122
+ - Origin cannot be `*` when using credentials
123
+
124
+ ### 3. Preview Deployments Blocked
125
+
126
+ **Symptoms**: Vercel preview URLs getting CORS errors
127
+
128
+ **Solutions**:
129
+ - Enhanced middleware should handle automatically
130
+ - Check regex patterns match your preview URL format
131
+ - Verify middleware is properly initialized
132
+
133
+ ## Security Considerations
134
+
135
+ 1. **Never use wildcard (`*`) in production** when credentials are enabled
136
+ 2. **Validate origins strictly** in production environments
137
+ 3. **Limit exposed headers** to only what's necessary
138
+ 4. **Use HTTPS** for all production origins
139
+ 5. **Implement rate limiting** alongside CORS
140
+
141
+ ## Frontend Configuration
142
+
143
+ ### Next.js (Vercel)
144
+
145
+ ```typescript
146
+ // Frontend API client configuration
147
+ const apiClient = axios.create({
148
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
149
+ withCredentials: true, // Important for cookies
150
+ headers: {
151
+ 'Content-Type': 'application/json'
152
+ }
153
+ });
154
+ ```
155
+
156
+ ### React SPA
157
+
158
+ ```javascript
159
+ // Fetch with credentials
160
+ fetch('https://api.cidadao.ai/endpoint', {
161
+ method: 'POST',
162
+ credentials: 'include', // Include cookies
163
+ headers: {
164
+ 'Content-Type': 'application/json'
165
+ },
166
+ body: JSON.stringify(data)
167
+ });
168
+ ```
169
+
170
+ ## Deployment Configurations
171
+
172
+ ### Nginx
173
+
174
+ Add to your server block:
175
+
176
+ ```nginx
177
+ # Handle preflight
178
+ if ($request_method = 'OPTIONS') {
179
+ add_header 'Access-Control-Allow-Origin' '$http_origin' always;
180
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
181
+ add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-API-Key' always;
182
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
183
+ add_header 'Access-Control-Max-Age' 86400 always;
184
+ return 204;
185
+ }
186
+ ```
187
+
188
+ ### CloudFlare
189
+
190
+ Create transform rules:
191
+
192
+ ```javascript
193
+ // Allow Vercel origins
194
+ if (http.request.headers["origin"][0] matches "^https://[a-zA-Z0-9-]+\\.vercel\\.app$") {
195
+ headers["Access-Control-Allow-Origin"] = http.request.headers["origin"][0]
196
+ headers["Access-Control-Allow-Credentials"] = "true"
197
+ }
198
+ ```
199
+
200
+ ## Monitoring
201
+
202
+ Monitor CORS issues through:
203
+
204
+ 1. **Application logs**: Look for `cors_origin_denied` events
205
+ 2. **Browser console**: Check for CORS policy errors
206
+ 3. **Network tab**: Inspect preflight OPTIONS requests
207
+ 4. **Metrics**: Track failed requests by origin
208
+
209
+ ## Support
210
+
211
+ For CORS-related issues:
212
+
213
+ 1. Check the [CORS validator output](#testing-cors)
214
+ 2. Review application logs for denied origins
215
+ 3. Verify frontend is sending correct headers
216
+ 4. Contact the platform team if patterns need updating
src/api/app.py CHANGED
@@ -161,15 +161,9 @@ app.add_middleware(
161
  # allowed_hosts=["localhost", "127.0.0.1", "*.cidadao.ai", "testserver"]
162
  # )
163
 
164
- # CORS middleware with secure configuration
165
- app.add_middleware(
166
- CORSMiddleware,
167
- allow_origins=settings.cors_origins,
168
- allow_credentials=settings.cors_allow_credentials,
169
- allow_methods=settings.cors_allow_methods,
170
- allow_headers=settings.cors_allow_headers,
171
- expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"]
172
- )
173
 
174
  # Add observability middleware
175
  app.add_middleware(CorrelationMiddleware, generate_request_id=True)
 
161
  # allowed_hosts=["localhost", "127.0.0.1", "*.cidadao.ai", "testserver"]
162
  # )
163
 
164
+ # Enhanced CORS middleware for Vercel integration
165
+ from src.api.middleware.cors_enhanced import setup_cors
166
+ setup_cors(app)
 
 
 
 
 
 
167
 
168
  # Add observability middleware
169
  app.add_middleware(CorrelationMiddleware, generate_request_id=True)
src/api/middleware/cors_enhanced.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module: api.middleware.cors_enhanced
3
+ Description: Enhanced CORS middleware for Vercel frontend integration
4
+ Author: Anderson H. Silva
5
+ Date: 2025-01-25
6
+ License: Proprietary - All rights reserved
7
+ """
8
+
9
+ import re
10
+ from typing import List, Optional, Set
11
+ from urllib.parse import urlparse
12
+
13
+ from fastapi import Request
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from starlette.middleware.base import BaseHTTPMiddleware
16
+ from starlette.responses import Response, PlainTextResponse
17
+ from starlette.types import ASGIApp
18
+
19
+ from src.core import get_logger
20
+ from src.core.config import settings
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class EnhancedCORSMiddleware(BaseHTTPMiddleware):
26
+ """
27
+ Enhanced CORS middleware with dynamic origin validation.
28
+
29
+ Features:
30
+ - Wildcard subdomain support
31
+ - Vercel preview deployment support
32
+ - Development/production mode awareness
33
+ - Custom header handling
34
+ - Preflight optimization
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ app,
40
+ allowed_origins: Optional[List[str]] = None,
41
+ allowed_origin_patterns: Optional[List[str]] = None,
42
+ allow_credentials: bool = True,
43
+ allowed_methods: Optional[List[str]] = None,
44
+ allowed_headers: Optional[List[str]] = None,
45
+ exposed_headers: Optional[List[str]] = None,
46
+ max_age: int = 3600
47
+ ):
48
+ """Initialize enhanced CORS middleware."""
49
+ super().__init__(app)
50
+
51
+ # Use settings if not provided
52
+ self.allowed_origins = set(allowed_origins or settings.cors_origins)
53
+ self.allowed_origin_patterns = allowed_origin_patterns or []
54
+ self.allow_credentials = allow_credentials
55
+ self.allowed_methods = allowed_methods or settings.cors_allow_methods
56
+ self.allowed_headers = allowed_headers or settings.cors_allow_headers
57
+ self.exposed_headers = exposed_headers or [
58
+ "X-RateLimit-Limit",
59
+ "X-RateLimit-Remaining",
60
+ "X-RateLimit-Reset",
61
+ "X-Request-ID",
62
+ "X-Correlation-ID"
63
+ ]
64
+ self.max_age = max_age
65
+
66
+ # Compile regex patterns
67
+ self.origin_patterns = []
68
+ for pattern in self.allowed_origin_patterns:
69
+ try:
70
+ self.origin_patterns.append(re.compile(pattern))
71
+ except re.error as e:
72
+ logger.error(f"Invalid CORS pattern: {pattern} - {e}")
73
+
74
+ # Add default Vercel patterns
75
+ self._add_vercel_patterns()
76
+
77
+ logger.info(
78
+ "enhanced_cors_initialized",
79
+ allowed_origins=list(self.allowed_origins),
80
+ pattern_count=len(self.origin_patterns),
81
+ allow_credentials=self.allow_credentials
82
+ )
83
+
84
+ def _add_vercel_patterns(self):
85
+ """Add Vercel-specific patterns."""
86
+ # Vercel preview deployments
87
+ vercel_patterns = [
88
+ r"^https://cidadao-ai-frontend-[a-zA-Z0-9]+-neural-thinker\.vercel\.app$",
89
+ r"^https://cidadao-ai-[a-zA-Z0-9]+-neural-thinker\.vercel\.app$",
90
+ r"^https://[a-zA-Z0-9-]+\.neural-thinker\.vercel\.app$"
91
+ ]
92
+
93
+ for pattern in vercel_patterns:
94
+ try:
95
+ self.origin_patterns.append(re.compile(pattern))
96
+ except re.error:
97
+ pass
98
+
99
+ async def dispatch(self, request: Request, call_next):
100
+ """Process request with enhanced CORS handling."""
101
+ origin = request.headers.get("origin")
102
+
103
+ # Handle preflight requests
104
+ if request.method == "OPTIONS":
105
+ response = await self._handle_preflight(request, origin)
106
+ if response:
107
+ return response
108
+
109
+ # Process regular request
110
+ response = await call_next(request)
111
+
112
+ # Add CORS headers if origin is allowed
113
+ if origin and self._is_origin_allowed(origin):
114
+ response.headers["Access-Control-Allow-Origin"] = origin
115
+ response.headers["Access-Control-Allow-Credentials"] = str(self.allow_credentials).lower()
116
+
117
+ # Add exposed headers
118
+ if self.exposed_headers:
119
+ response.headers["Access-Control-Expose-Headers"] = ", ".join(self.exposed_headers)
120
+
121
+ # Add Vary header for caching
122
+ vary_headers = response.headers.get("Vary", "").split(", ")
123
+ if "Origin" not in vary_headers:
124
+ vary_headers.append("Origin")
125
+ response.headers["Vary"] = ", ".join(filter(None, vary_headers))
126
+
127
+ return response
128
+
129
+ async def _handle_preflight(self, request: Request, origin: Optional[str]) -> Optional[Response]:
130
+ """Handle preflight OPTIONS requests."""
131
+ if not origin or not self._is_origin_allowed(origin):
132
+ return None
133
+
134
+ # Get requested method and headers
135
+ requested_method = request.headers.get("Access-Control-Request-Method")
136
+ requested_headers = request.headers.get("Access-Control-Request-Headers", "").split(", ")
137
+
138
+ # Validate method
139
+ if requested_method and requested_method not in self.allowed_methods:
140
+ logger.warning(
141
+ "cors_preflight_method_denied",
142
+ origin=origin,
143
+ method=requested_method
144
+ )
145
+ return PlainTextResponse(
146
+ "Method not allowed",
147
+ status_code=403
148
+ )
149
+
150
+ # Build response
151
+ response = PlainTextResponse("OK", status_code=200)
152
+
153
+ # Set CORS headers
154
+ response.headers["Access-Control-Allow-Origin"] = origin
155
+ response.headers["Access-Control-Allow-Methods"] = ", ".join(self.allowed_methods)
156
+ response.headers["Access-Control-Allow-Headers"] = ", ".join(self._get_allowed_headers(requested_headers))
157
+ response.headers["Access-Control-Allow-Credentials"] = str(self.allow_credentials).lower()
158
+ response.headers["Access-Control-Max-Age"] = str(self.max_age)
159
+
160
+ return response
161
+
162
+ def _is_origin_allowed(self, origin: str) -> bool:
163
+ """Check if origin is allowed."""
164
+ # Exact match
165
+ if origin in self.allowed_origins:
166
+ return True
167
+
168
+ # Wildcard check (for backwards compatibility)
169
+ for allowed in self.allowed_origins:
170
+ if allowed == "*":
171
+ return True
172
+ if allowed.startswith("https://*.") or allowed.startswith("http://*."):
173
+ # Simple wildcard subdomain check
174
+ base_domain = allowed.replace("https://*.", "").replace("http://*.", "")
175
+ parsed = urlparse(origin)
176
+ if parsed.hostname and parsed.hostname.endswith(base_domain):
177
+ return True
178
+
179
+ # Regex pattern match
180
+ for pattern in self.origin_patterns:
181
+ if pattern.match(origin):
182
+ return True
183
+
184
+ # Development mode - allow localhost
185
+ if settings.is_development:
186
+ parsed = urlparse(origin)
187
+ if parsed.hostname in ["localhost", "127.0.0.1", "::1"]:
188
+ return True
189
+
190
+ logger.debug(
191
+ "cors_origin_denied",
192
+ origin=origin,
193
+ allowed_count=len(self.allowed_origins)
194
+ )
195
+
196
+ return False
197
+
198
+ def _get_allowed_headers(self, requested_headers: List[str]) -> List[str]:
199
+ """Get allowed headers for response."""
200
+ if "*" in self.allowed_headers:
201
+ # Allow all requested headers
202
+ return requested_headers
203
+
204
+ # Filter to allowed headers
205
+ allowed = set(h.lower() for h in self.allowed_headers)
206
+ return [h for h in requested_headers if h.lower() in allowed]
207
+
208
+
209
+ def setup_cors(app: ASGIApp) -> None:
210
+ """
211
+ Setup CORS for the application with enhanced configuration.
212
+
213
+ This replaces the default CORSMiddleware with our enhanced version.
214
+ """
215
+ # Remove existing CORS middleware if present
216
+ middlewares = []
217
+ for middleware in app.middleware:
218
+ if not isinstance(middleware, CORSMiddleware):
219
+ middlewares.append(middleware)
220
+ app.middleware = middlewares
221
+
222
+ # Add enhanced CORS middleware
223
+ app.add_middleware(
224
+ EnhancedCORSMiddleware,
225
+ allowed_origins=settings.cors_origins,
226
+ allowed_origin_patterns=[
227
+ # Vercel preview deployments
228
+ r"^https://cidadao-ai-frontend-[a-zA-Z0-9]+-.*\.vercel\.app$",
229
+ r"^https://cidadao-ai-[a-zA-Z0-9]+-.*\.vercel\.app$",
230
+ # GitHub Codespaces
231
+ r"^https://.*\.github\.dev$",
232
+ r"^https://.*\.gitpod\.io$",
233
+ # Local development with custom ports
234
+ r"^http://localhost:[0-9]+$",
235
+ r"^http://127\.0\.0\.1:[0-9]+$"
236
+ ],
237
+ allow_credentials=settings.cors_allow_credentials,
238
+ allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
239
+ allowed_headers=[
240
+ "Accept",
241
+ "Accept-Language",
242
+ "Content-Language",
243
+ "Content-Type",
244
+ "Authorization",
245
+ "X-API-Key",
246
+ "X-Request-ID",
247
+ "X-Correlation-ID",
248
+ "X-CSRF-Token",
249
+ "X-Requested-With",
250
+ "Cache-Control",
251
+ "If-Match",
252
+ "If-None-Match",
253
+ "If-Modified-Since",
254
+ "If-Unmodified-Since"
255
+ ],
256
+ exposed_headers=[
257
+ "X-RateLimit-Limit",
258
+ "X-RateLimit-Remaining",
259
+ "X-RateLimit-Reset",
260
+ "X-RateLimit-Window",
261
+ "X-Request-ID",
262
+ "X-Correlation-ID",
263
+ "X-Total-Count",
264
+ "Link",
265
+ "ETag",
266
+ "Last-Modified",
267
+ "Cache-Control"
268
+ ],
269
+ max_age=86400 # 24 hours for production
270
+ )
src/core/config.py CHANGED
@@ -159,19 +159,34 @@ class Settings(BaseSettings):
159
  cors_origins: List[str] = Field(
160
  default=[
161
  "http://localhost:3000",
162
- "http://localhost:8000",
 
 
163
  "https://cidadao-ai-frontend.vercel.app",
 
164
  "https://*.vercel.app",
165
- "https://neural-thinker-cidadao-ai-backend.hf.space"
 
166
  ],
167
  description="CORS allowed origins"
168
  )
169
  cors_allow_credentials: bool = Field(default=True, description="Allow credentials")
170
  cors_allow_methods: List[str] = Field(
171
- default=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
172
  description="Allowed methods"
173
  )
174
  cors_allow_headers: List[str] = Field(default=["*"], description="Allowed headers")
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  # Rate Limiting
177
  rate_limit_per_minute: int = Field(default=60, description="Rate limit per minute")
 
159
  cors_origins: List[str] = Field(
160
  default=[
161
  "http://localhost:3000",
162
+ "http://localhost:3001",
163
+ "http://localhost:8000",
164
+ "http://127.0.0.1:3000",
165
  "https://cidadao-ai-frontend.vercel.app",
166
+ "https://cidadao-ai.vercel.app",
167
  "https://*.vercel.app",
168
+ "https://neural-thinker-cidadao-ai-backend.hf.space",
169
+ "https://*.hf.space"
170
  ],
171
  description="CORS allowed origins"
172
  )
173
  cors_allow_credentials: bool = Field(default=True, description="Allow credentials")
174
  cors_allow_methods: List[str] = Field(
175
+ default=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
176
  description="Allowed methods"
177
  )
178
  cors_allow_headers: List[str] = Field(default=["*"], description="Allowed headers")
179
+ cors_exposed_headers: List[str] = Field(
180
+ default=[
181
+ "X-RateLimit-Limit",
182
+ "X-RateLimit-Remaining",
183
+ "X-RateLimit-Reset",
184
+ "X-Request-ID",
185
+ "X-Total-Count"
186
+ ],
187
+ description="Exposed headers"
188
+ )
189
+ cors_max_age: int = Field(default=3600, description="CORS max age in seconds")
190
 
191
  # Rate Limiting
192
  rate_limit_per_minute: int = Field(default=60, description="Rate limit per minute")
src/utils/cors_validator.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module: utils.cors_validator
3
+ Description: CORS configuration validator and tester
4
+ Author: Anderson H. Silva
5
+ Date: 2025-01-25
6
+ License: Proprietary - All rights reserved
7
+ """
8
+
9
+ import httpx
10
+ from typing import Dict, List, Optional, Tuple
11
+ from urllib.parse import urlparse
12
+
13
+ from src.core import get_logger
14
+ from src.core.config import settings
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class CORSValidator:
20
+ """Validate and test CORS configuration."""
21
+
22
+ def __init__(self, base_url: str = "http://localhost:8000"):
23
+ """Initialize CORS validator."""
24
+ self.base_url = base_url
25
+ self.test_endpoints = [
26
+ "/",
27
+ "/health",
28
+ "/api/v1/chat/message",
29
+ "/api/v1/investigations/analyze",
30
+ "/api/v1/auth/login"
31
+ ]
32
+
33
+ async def validate_origin(
34
+ self,
35
+ origin: str,
36
+ endpoint: str = "/health",
37
+ method: str = "GET"
38
+ ) -> Tuple[bool, Dict[str, str]]:
39
+ """
40
+ Validate if origin is allowed by CORS policy.
41
+
42
+ Returns:
43
+ Tuple of (is_allowed, cors_headers)
44
+ """
45
+ headers = {
46
+ "Origin": origin,
47
+ "User-Agent": "CORS-Validator/1.0"
48
+ }
49
+
50
+ async with httpx.AsyncClient() as client:
51
+ try:
52
+ # Send preflight request
53
+ preflight_response = await client.options(
54
+ f"{self.base_url}{endpoint}",
55
+ headers={
56
+ **headers,
57
+ "Access-Control-Request-Method": method,
58
+ "Access-Control-Request-Headers": "Content-Type, Authorization"
59
+ }
60
+ )
61
+
62
+ # Extract CORS headers
63
+ cors_headers = {}
64
+ for header in preflight_response.headers:
65
+ if header.lower().startswith("access-control-"):
66
+ cors_headers[header] = preflight_response.headers[header]
67
+
68
+ # Check if origin is allowed
69
+ allowed_origin = cors_headers.get("Access-Control-Allow-Origin", "")
70
+ is_allowed = allowed_origin == origin or allowed_origin == "*"
71
+
72
+ logger.info(
73
+ "cors_validation_result",
74
+ origin=origin,
75
+ endpoint=endpoint,
76
+ is_allowed=is_allowed,
77
+ cors_headers=cors_headers
78
+ )
79
+
80
+ return is_allowed, cors_headers
81
+
82
+ except Exception as e:
83
+ logger.error(
84
+ "cors_validation_error",
85
+ origin=origin,
86
+ endpoint=endpoint,
87
+ error=str(e)
88
+ )
89
+ return False, {}
90
+
91
+ async def test_all_origins(self) -> Dict[str, Dict[str, any]]:
92
+ """Test all configured origins."""
93
+ results = {}
94
+
95
+ # Test configured origins
96
+ for origin in settings.cors_origins:
97
+ if origin == "*" or origin.startswith("https://*."):
98
+ # Skip wildcards
99
+ continue
100
+
101
+ is_allowed, headers = await self.validate_origin(origin)
102
+ results[origin] = {
103
+ "allowed": is_allowed,
104
+ "headers": headers
105
+ }
106
+
107
+ # Test common Vercel preview URLs
108
+ vercel_test_origins = [
109
+ "https://cidadao-ai-frontend-abc123-neural-thinker.vercel.app",
110
+ "https://cidadao-ai-preview-xyz789-neural-thinker.vercel.app"
111
+ ]
112
+
113
+ for origin in vercel_test_origins:
114
+ is_allowed, headers = await self.validate_origin(origin)
115
+ results[origin] = {
116
+ "allowed": is_allowed,
117
+ "headers": headers,
118
+ "note": "Vercel preview URL test"
119
+ }
120
+
121
+ return results
122
+
123
+ def generate_nginx_config(self) -> str:
124
+ """Generate nginx CORS configuration."""
125
+ config = """# CORS configuration for Cidadão.AI
126
+ # Add this to your nginx server block
127
+
128
+ # Handle preflight requests
129
+ if ($request_method = 'OPTIONS') {
130
+ add_header 'Access-Control-Allow-Origin' '$http_origin' always;
131
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
132
+ add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-API-Key' always;
133
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
134
+ add_header 'Access-Control-Max-Age' 86400 always;
135
+ add_header 'Content-Length' 0;
136
+ add_header 'Content-Type' 'text/plain charset=UTF-8';
137
+ return 204;
138
+ }
139
+
140
+ # Add CORS headers to responses
141
+ add_header 'Access-Control-Allow-Origin' '$http_origin' always;
142
+ add_header 'Access-Control-Allow-Credentials' 'true' always;
143
+ add_header 'Access-Control-Expose-Headers' 'X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,X-Request-ID,X-Total-Count' always;
144
+ """
145
+ return config
146
+
147
+ def generate_cloudflare_headers(self) -> List[Dict[str, str]]:
148
+ """Generate Cloudflare transform rules for CORS."""
149
+ rules = []
150
+
151
+ # Allow Vercel origins
152
+ rules.append({
153
+ "name": "CORS - Vercel Origins",
154
+ "expression": 'http.request.headers["origin"][0] matches "^https://[a-zA-Z0-9-]+\\.vercel\\.app$"',
155
+ "headers": {
156
+ "Access-Control-Allow-Origin": '${http.request.headers["origin"][0]}',
157
+ "Access-Control-Allow-Credentials": "true",
158
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS"
159
+ }
160
+ })
161
+
162
+ # Allow localhost for development
163
+ rules.append({
164
+ "name": "CORS - Localhost Development",
165
+ "expression": 'http.request.headers["origin"][0] in {"http://localhost:3000", "http://127.0.0.1:3000"}',
166
+ "headers": {
167
+ "Access-Control-Allow-Origin": '${http.request.headers["origin"][0]}',
168
+ "Access-Control-Allow-Credentials": "true",
169
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS"
170
+ }
171
+ })
172
+
173
+ return rules
174
+
175
+ async def test_credentials_flow(
176
+ self,
177
+ origin: str = "https://cidadao-ai-frontend.vercel.app"
178
+ ) -> Dict[str, any]:
179
+ """Test CORS with credentials (cookies/auth)."""
180
+ results = {
181
+ "origin": origin,
182
+ "tests": {}
183
+ }
184
+
185
+ async with httpx.AsyncClient() as client:
186
+ # Test login endpoint
187
+ try:
188
+ response = await client.post(
189
+ f"{self.base_url}/api/v1/auth/login",
190
+ headers={"Origin": origin},
191
+ json={"email": "[email protected]", "password": "test"},
192
+ follow_redirects=False
193
+ )
194
+
195
+ results["tests"]["login"] = {
196
+ "status": response.status_code,
197
+ "cors_origin": response.headers.get("Access-Control-Allow-Origin"),
198
+ "cors_credentials": response.headers.get("Access-Control-Allow-Credentials"),
199
+ "has_cookies": "Set-Cookie" in response.headers
200
+ }
201
+ except Exception as e:
202
+ results["tests"]["login"] = {"error": str(e)}
203
+
204
+ # Test authenticated endpoint
205
+ try:
206
+ response = await client.get(
207
+ f"{self.base_url}/api/v1/chat/history",
208
+ headers={
209
+ "Origin": origin,
210
+ "Authorization": "Bearer test-token"
211
+ }
212
+ )
213
+
214
+ results["tests"]["authenticated"] = {
215
+ "status": response.status_code,
216
+ "cors_origin": response.headers.get("Access-Control-Allow-Origin"),
217
+ "cors_credentials": response.headers.get("Access-Control-Allow-Credentials")
218
+ }
219
+ except Exception as e:
220
+ results["tests"]["authenticated"] = {"error": str(e)}
221
+
222
+ return results
223
+
224
+
225
+ # CLI utility
226
+ async def main():
227
+ """Run CORS validation tests."""
228
+ import asyncio
229
+ import json
230
+
231
+ validator = CORSValidator()
232
+
233
+ print("🔍 Testing CORS configuration...\n")
234
+
235
+ # Test all origins
236
+ print("1. Testing configured origins:")
237
+ results = await validator.test_all_origins()
238
+ for origin, result in results.items():
239
+ status = "✅" if result["allowed"] else "❌"
240
+ print(f" {status} {origin}")
241
+
242
+ # Test credentials flow
243
+ print("\n2. Testing credentials flow:")
244
+ creds_results = await validator.test_credentials_flow()
245
+ print(json.dumps(creds_results, indent=2))
246
+
247
+ # Generate configs
248
+ print("\n3. Generated nginx configuration:")
249
+ print(validator.generate_nginx_config())
250
+
251
+ print("\n4. Cloudflare transform rules:")
252
+ cf_rules = validator.generate_cloudflare_headers()
253
+ print(json.dumps(cf_rules, indent=2))
254
+
255
+
256
+ if __name__ == "__main__":
257
+ import asyncio
258
+ asyncio.run(main())