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]
|