Skip to content

Commit 667a3e1

Browse files
committed
add OAuth state validation, secure cookie config, request tracing middleware
1 parent 7177ec9 commit 667a3e1

3 files changed

Lines changed: 103 additions & 8 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
APP_ENV=development
22
SECRET_KEY=replace-with-a-long-random-string
3+
FRONTEND_URL=http://localhost:5173
4+
SESSION_TTL_SECONDS=3600
35
GITHUB_CLIENT_ID=your_github_client_id
46
GITHUB_CLIENT_SECRET=your_github_client_secret
57
GITHUB_TOKEN=your_github_token
68
OLLAMA_URL=http://127.0.0.1:11434
9+
OLLAMA_MODEL=phi3:mini
10+
OLLAMA_FALLBACK_MODELS=
711
CELERY_BROKER_URL=redis://localhost:6379/0
812
CELERY_RESULT_BACKEND=redis://localhost:6379/0

PRODUCTION_READINESS.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Production Readiness Checklist
2+
3+
## 1. Security
4+
- [x] `.env` excluded from git.
5+
- [x] `.env.example` provided with non-secret placeholders.
6+
- [x] Backend enforces `SECRET_KEY` outside development mode.
7+
- [x] OAuth state validation implemented using signed cookie.
8+
- [ ] Rotate all previously exposed GitHub credentials/tokens.
9+
- [ ] Move secrets to deployment secret manager (not file-based on server).
10+
11+
## 2. Auth & Sessions
12+
- [x] Session cookie uses environment-based `secure` and `samesite`.
13+
- [x] Session TTL configurable (`SESSION_TTL_SECONDS`).
14+
- [ ] Replace `sessions.json` with Redis/DB-backed session store.
15+
- [ ] Add CSRF protections for state-changing endpoints.
16+
17+
## 3. Reliability
18+
- [x] AI model fallback and status endpoint (`/api/ai-status`) added.
19+
- [x] Recursive file tree endpoint with graceful fallback.
20+
- [x] Better GitHub/Ollama error messages exposed to frontend.
21+
- [ ] Add circuit-breaker/retry policy for repeated upstream failures.
22+
23+
## 4. Observability
24+
- [x] Request ID added on all responses (`X-Request-ID`).
25+
- [x] Basic request latency logging middleware added.
26+
- [ ] Centralized structured logging sink (ELK/Datadog/etc.).
27+
- [ ] Error alerting and uptime checks.
28+
29+
## 5. Quality Gates
30+
- [x] Static checks currently passing (`py_compile`, frontend lint).
31+
- [ ] Add backend API tests (auth, files, chat).
32+
- [ ] Add frontend integration tests (repo tree + file preview + chat).
33+
- [ ] CI pipeline enforcing lint/tests before merge.
34+
35+
## Recommended Next Sprint
36+
1. Replace file-based session store with Redis.
37+
2. Add automated tests for core flows.
38+
3. Add deployment config for managed secrets and observability.

main.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33
import asyncio
4+
import logging
45
from fastapi import FastAPI, Depends, HTTPException, Response, Request
56
from fastapi.responses import RedirectResponse, JSONResponse
67
from fastapi.middleware.cors import CORSMiddleware
@@ -29,8 +30,14 @@
2930
OLLAMA_FALLBACK_MODELS = [
3031
m.strip() for m in os.getenv("OLLAMA_FALLBACK_MODELS", "").split(",") if m.strip()
3132
]
32-
33+
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
34+
SESSION_TTL_SECONDS = int(os.getenv("SESSION_TTL_SECONDS", "3600"))
35+
COOKIE_SECURE = APP_ENV != "development"
36+
COOKIE_SAMESITE = "Lax" if APP_ENV == "development" else "Strict"
3337
app = FastAPI()
38+
logger = logging.getLogger("codescribe")
39+
if not logger.handlers:
40+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
3441

3542
# ---- Session persistence ----
3643
SESSIONS_FILE = "sessions.json"
@@ -86,6 +93,15 @@ def save_sessions():
8693
allow_headers=["*"]
8794
)
8895

96+
@app.middleware("http")
97+
async def add_request_context(request: Request, call_next):
98+
req_id = request.headers.get("x-request-id") or str(uuid4())
99+
start = time.perf_counter()
100+
response = await call_next(request)
101+
elapsed_ms = int((time.perf_counter() - start) * 1000)
102+
response.headers["X-Request-ID"] = req_id
103+
logger.info("%s %s -> %s (%sms) req_id=%s", request.method, request.url.path, response.status_code, elapsed_ms, req_id)
104+
return response
89105
# ---- Env vars ----
90106
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
91107
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")
@@ -99,6 +115,20 @@ def ensure_github_oauth_config():
99115
)
100116

101117

118+
def validate_oauth_state(request: Request, state: Optional[str]):
119+
if not state:
120+
raise HTTPException(status_code=400, detail="Missing state parameter")
121+
signed_state = request.cookies.get("oauth_state")
122+
if not signed_state:
123+
raise HTTPException(status_code=400, detail="Missing OAuth state cookie")
124+
try:
125+
expected_state = serializer.loads(signed_state).get("state")
126+
except Exception:
127+
raise HTTPException(status_code=400, detail="Invalid OAuth state cookie")
128+
if expected_state != state:
129+
raise HTTPException(status_code=400, detail="OAuth state mismatch")
130+
131+
102132
# ---------- NEW: tiny in-memory TTL cache for GitHub responses ----------
103133
from functools import lru_cache
104134
from collections import defaultdict
@@ -203,13 +233,24 @@ async def test_auth(user=Depends(get_current_user)):
203233
async def github_login():
204234
ensure_github_oauth_config()
205235
state = os.urandom(16).hex()
206-
return RedirectResponse(
236+
response = RedirectResponse(
207237
f"https://github.com/login/oauth/authorize?"
208238
f"client_id={GITHUB_CLIENT_ID}&state={state}&scope=repo,user"
209239
)
240+
response.set_cookie(
241+
key="oauth_state",
242+
value=serializer.dumps({"state": state}),
243+
httponly=True,
244+
samesite=COOKIE_SAMESITE,
245+
secure=COOKIE_SECURE,
246+
max_age=300,
247+
)
248+
return response
210249

211250
@app.get("/auth/github/callback")
212-
async def github_callback(code: str, state: Optional[str] = None):
251+
async def github_callback(request: Request, code: str, state: Optional[str] = None):
252+
ensure_github_oauth_config()
253+
validate_oauth_state(request, state)
213254
ensure_github_oauth_config()
214255
if not state:
215256
raise HTTPException(status_code=400, detail="Missing state parameter")
@@ -235,19 +276,21 @@ async def github_callback(code: str, state: Optional[str] = None):
235276
"access_token": token_data["access_token"],
236277
"user": user_data["login"],
237278
"user_id": user_data["id"],
238-
"expires": time.time() + 3600
279+
"expires": time.time() + SESSION_TTL_SECONDS
239280
}
240281
save_sessions()
241282

242283
signed_cookie = serializer.dumps({"session_id": session_id})
243-
response = RedirectResponse(url="http://localhost:5173")
284+
response = RedirectResponse(url=FRONTEND_URL)
244285
response.set_cookie(
245286
key="session_id",
246287
value=signed_cookie,
247288
httponly=True,
248-
samesite="Lax",
249-
secure=False,
289+
samesite=COOKIE_SAMESITE,
290+
secure=COOKIE_SECURE,
291+
max_age=SESSION_TTL_SECONDS,
250292
)
293+
response.delete_cookie("oauth_state", samesite=COOKIE_SAMESITE, secure=COOKIE_SECURE)
251294
return response
252295

253296
async def get_repo_default_branch(owner: str, repo: str) -> str:
@@ -872,7 +915,7 @@ async def logout(request: Request, user=Depends(get_current_user)):
872915

873916
# Clear cookie
874917
response = JSONResponse({"message": "Logged out"})
875-
response.delete_cookie("session_id")
918+
response.delete_cookie("session_id", samesite=COOKIE_SAMESITE, secure=COOKIE_SECURE)
876919
return response
877920

878921
@app.get("/repos/{owner}/{repo}/file-content")
@@ -1000,3 +1043,13 @@ async def get_task_status(task_id: str):
10001043

10011044

10021045

1046+
1047+
1048+
1049+
1050+
1051+
1052+
1053+
1054+
1055+

0 commit comments

Comments
 (0)