From 224635b88deadd7f1ccfb8b3a973f22c46a70402 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Wed, 8 Apr 2026 12:08:52 +0200 Subject: [PATCH] security: fix auth (argon2, cookie, JWT), CORS, session middleware, pyproject.toml --- app/auth/dependencies.py | 2 +- app/auth/jwt.py | 8 ++++++++ app/auth/providers/google.py | 2 ++ app/auth/providers/password.py | 10 +++------- app/auth/router.py | 8 ++------ app/main.py | 10 +++++----- pyproject.toml | 2 ++ 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py index 1642d0a..a0ed29f 100644 --- a/app/auth/dependencies.py +++ b/app/auth/dependencies.py @@ -65,7 +65,7 @@ async def get_optional_user( if not authorization or not authorization.startswith("Bearer "): return None try: - return await get_current_user(authorization, conn) + return await get_current_user(request=None, authorization=authorization, conn=conn) except HTTPException: return None diff --git a/app/auth/jwt.py b/app/auth/jwt.py index 943438e..a09f4e4 100644 --- a/app/auth/jwt.py +++ b/app/auth/jwt.py @@ -7,6 +7,14 @@ from typing import Optional import jwt SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "dev-jwt-secret-change-in-production") + +import warnings as _warnings +if len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", "true"): + _warnings.warn( + f"JWT_SECRET_KEY is {len(SECRET_KEY)} chars — minimum 32 recommended for production", + UserWarning, stacklevel=2, + ) + ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_HOURS = 24 * 30 # 30 days diff --git a/app/auth/providers/google.py b/app/auth/providers/google.py index aeeb8c5..e26264e 100644 --- a/app/auth/providers/google.py +++ b/app/auth/providers/google.py @@ -89,10 +89,12 @@ async def google_callback(request: Request): jwt_token = create_access_token(user["id"], user["email"], user["role"]) # Redirect to dashboard with token in cookie + is_https = request.url.scheme == "https" response = RedirectResponse(url="/dashboard", status_code=302) response.set_cookie( key="access_token", value=jwt_token, httponly=True, max_age=86400 * 30, samesite="lax", + secure=is_https, ) return response diff --git a/app/auth/providers/password.py b/app/auth/providers/password.py index 3cf81a6..d17df6b 100644 --- a/app/auth/providers/password.py +++ b/app/auth/providers/password.py @@ -68,13 +68,9 @@ async def password_setup( raise HTTPException(status_code=400, detail="Invalid setup token") # Hash and save password - try: - from argon2 import PasswordHasher - ph = PasswordHasher() - hashed = ph.hash(request.password) - except ImportError: - import hashlib - hashed = hashlib.sha256(request.password.encode()).hexdigest() + from argon2 import PasswordHasher + ph = PasswordHasher() + hashed = ph.hash(request.password) repo.update(id=user["id"], password_hash=hashed, setup_token=None) token = create_access_token(user["id"], user["email"], user["role"]) diff --git a/app/auth/router.py b/app/auth/router.py index d381c50..8491212 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -88,12 +88,8 @@ async def bootstrap( user_id = str(uuid.uuid4()) password_hash = None if request.password: - try: - from argon2 import PasswordHasher - password_hash = PasswordHasher().hash(request.password) - except ImportError: - import hashlib - password_hash = hashlib.sha256(request.password.encode()).hexdigest() + from argon2 import PasswordHasher + password_hash = PasswordHasher().hash(request.password) repo.create( id=user_id, diff --git a/app/main.py b/app/main.py index 397fb6e..c27f22d 100644 --- a/app/main.py +++ b/app/main.py @@ -39,15 +39,15 @@ def create_app() -> FastAPI: ) # Session middleware (required for OAuth state) - app.add_middleware( - SessionMiddleware, - secret_key=os.environ.get("JWT_SECRET_KEY", "dev-session-secret"), - ) + import secrets as _secrets + session_secret = os.environ.get("SESSION_SECRET", os.environ.get("JWT_SECRET_KEY", _secrets.token_hex(32))) + app.add_middleware(SessionMiddleware, secret_key=session_secret) # CORS for CLI and external clients + cors_origins = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:8000").split(",") app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[o.strip() for o in cors_origins], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/pyproject.toml b/pyproject.toml index d353dfa..0e9c266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,8 @@ dependencies = [ # Authentication "PyJWT>=2.8.0", "itsdangerous>=2.1.0", + "authlib>=1.3.0", + "argon2-cffi>=23.1.0", # HTTP client "httpx>=0.27.0", # CLI