feat(security): configure CORS and API validation
Browse files- app/main.py +34 -9
- requirements.txt +5 -1
app/main.py
CHANGED
|
@@ -8,6 +8,7 @@ import time
|
|
| 8 |
from pathlib import Path
|
| 9 |
from fastapi.security.api_key import APIKeyHeader
|
| 10 |
import os
|
|
|
|
| 11 |
|
| 12 |
# Load environment variables
|
| 13 |
load_dotenv()
|
|
@@ -24,10 +25,26 @@ FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000") # Add default
|
|
| 24 |
|
| 25 |
async def get_api_key(api_key_header: str = Security(api_key_header)):
|
| 26 |
if not API_KEY:
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
if api_key_header
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
return api_key_header
|
| 33 |
|
|
@@ -87,15 +104,23 @@ async def security_headers(request: Request, call_next):
|
|
| 87 |
response = await call_next(request)
|
| 88 |
|
| 89 |
response.headers["X-Content-Type-Options"] = "nosniff"
|
| 90 |
-
response.headers["X-Frame-Options"] = "
|
| 91 |
response.headers["X-XSS-Protection"] = "1; mode=block"
|
| 92 |
-
response.headers["Strict-Transport-Security"] =
|
| 93 |
-
"max-age=31536000; includeSubDomains"
|
| 94 |
-
)
|
| 95 |
response.headers["Content-Security-Policy"] = (
|
| 96 |
-
"default-src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
)
|
| 98 |
-
response.headers["
|
|
|
|
|
|
|
| 99 |
|
| 100 |
return response
|
| 101 |
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
from fastapi.security.api_key import APIKeyHeader
|
| 10 |
import os
|
| 11 |
+
import secrets
|
| 12 |
|
| 13 |
# Load environment variables
|
| 14 |
load_dotenv()
|
|
|
|
| 25 |
|
| 26 |
async def get_api_key(api_key_header: str = Security(api_key_header)):
|
| 27 |
if not API_KEY:
|
| 28 |
+
logger.error("API key not configured on server")
|
| 29 |
+
raise HTTPException(
|
| 30 |
+
status_code=500,
|
| 31 |
+
detail="Server configuration error" # Don't expose specific details
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
if not api_key_header:
|
| 35 |
+
raise HTTPException(
|
| 36 |
+
status_code=401,
|
| 37 |
+
detail="Missing API key",
|
| 38 |
+
headers={"WWW-Authenticate": "ApiKey"},
|
| 39 |
+
)
|
| 40 |
|
| 41 |
+
if not secrets.compare_digest(api_key_header, API_KEY): # Constant-time comparison
|
| 42 |
+
logger.warning(f"Invalid API key attempt from {request.client.host}")
|
| 43 |
+
raise HTTPException(
|
| 44 |
+
status_code=403,
|
| 45 |
+
detail="Invalid authentication credentials",
|
| 46 |
+
headers={"WWW-Authenticate": "ApiKey"},
|
| 47 |
+
)
|
| 48 |
|
| 49 |
return api_key_header
|
| 50 |
|
|
|
|
| 104 |
response = await call_next(request)
|
| 105 |
|
| 106 |
response.headers["X-Content-Type-Options"] = "nosniff"
|
| 107 |
+
response.headers["X-Frame-Options"] = "DENY" # Stricter than SAMEORIGIN
|
| 108 |
response.headers["X-XSS-Protection"] = "1; mode=block"
|
| 109 |
+
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
|
|
|
|
|
|
|
| 110 |
response.headers["Content-Security-Policy"] = (
|
| 111 |
+
"default-src 'self'; "
|
| 112 |
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
| 113 |
+
"style-src 'self' 'unsafe-inline'; "
|
| 114 |
+
"img-src 'self' data: https:; "
|
| 115 |
+
"connect-src 'self' "
|
| 116 |
+
)
|
| 117 |
+
response.headers["Permissions-Policy"] = (
|
| 118 |
+
"accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
|
| 119 |
+
"magnetometer=(), microphone=(), payment=(), usb=()"
|
| 120 |
)
|
| 121 |
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate"
|
| 122 |
+
response.headers["Pragma"] = "no-cache"
|
| 123 |
+
response.headers["Expires"] = "0"
|
| 124 |
|
| 125 |
return response
|
| 126 |
|
requirements.txt
CHANGED
|
@@ -7,4 +7,8 @@ mistralai>=0.0.7
|
|
| 7 |
python-dotenv>=1.0.0
|
| 8 |
langchain>=0.3.15
|
| 9 |
langchain-mistralai>=0.2.4
|
| 10 |
-
elevenlabs>=0.1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
python-dotenv>=1.0.0
|
| 8 |
langchain>=0.3.15
|
| 9 |
langchain-mistralai>=0.2.4
|
| 10 |
+
elevenlabs>=0.1.0
|
| 11 |
+
slowapi
|
| 12 |
+
cryptography
|
| 13 |
+
python-jose[cryptography]
|
| 14 |
+
passlib[bcrypt]
|