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 +216 -0
- src/api/app.py +3 -9
- src/api/middleware/cors_enhanced.py +270 -0
- src/core/config.py +18 -3
- src/utils/cors_validator.py +258 -0
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
|
| 165 |
-
|
| 166 |
-
|
| 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:
|
|
|
|
|
|
|
| 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())
|