Rate-limit logins, constant-time creds, pin CORS, secure cookie

Both login endpoints now share verify_admin_credentials (hmac
compare_digest, SEC-02) and a 5/min/IP sliding-window rate limit that
returns 429 with Retry-After (SEC-03). CORS is pinned to the prod
domain and localhost dev origins instead of '*' (SEC-04). The session
cookie's Secure flag is driven by COOKIE_SECURE instead of hardcoded
false (SEC-08).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-09 19:13:19 -06:00
parent 15ea1ef4c2
commit b4123703ef
3 changed files with 79 additions and 25 deletions

View File

@@ -5,7 +5,7 @@ import uuid
from contextlib import asynccontextmanager
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from fastapi import FastAPI, HTTPException, Request, Response, status
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from jose import JWTError, jwt
from pydantic import BaseModel
@@ -13,7 +13,12 @@ from pydantic import BaseModel
from app.agent.agent import build_agent
from app.agent.tools import reset_session, set_session
from app.api.v1.router import api_router
from app.auth import ALGORITHM, create_access_token
from app.auth import (
ALGORITHM,
check_login_rate_limit,
create_access_token,
verify_admin_credentials,
)
from app.config import settings
from app.db import get_session, init_db, run_migrations
from app.seed import seed_db
@@ -78,10 +83,10 @@ app = FastAPI(title="WealthySmart API", version="0.1.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
allow_headers=["content-type", "authorization"],
)
@@ -161,12 +166,9 @@ class LoginRequest(BaseModel):
@app.post("/api/auth/login")
def cookie_login(body: LoginRequest, response: Response):
if (
body.username != settings.ADMIN_USERNAME
or body.password != settings.ADMIN_PASSWORD
):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
def cookie_login(body: LoginRequest, request: Request, response: Response):
check_login_rate_limit(request)
verify_admin_credentials(body.username, body.password)
token = create_access_token(body.username)
response.set_cookie(
key="ws_token",
@@ -174,7 +176,7 @@ def cookie_login(body: LoginRequest, response: Response):
httponly=True,
samesite="lax",
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
secure=False, # set True behind TLS in production via nginx
secure=settings.COOKIE_SECURE,
)
return {"ok": True}