security: fix auth (argon2, cookie, JWT), CORS, session middleware, pyproject.toml

This commit is contained in:
ZdenekSrotyr 2026-04-08 12:08:52 +02:00
parent d5659d7091
commit 224635b88d
7 changed files with 23 additions and 19 deletions

View file

@ -65,7 +65,7 @@ async def get_optional_user(
if not authorization or not authorization.startswith("Bearer "): if not authorization or not authorization.startswith("Bearer "):
return None return None
try: try:
return await get_current_user(authorization, conn) return await get_current_user(request=None, authorization=authorization, conn=conn)
except HTTPException: except HTTPException:
return None return None

View file

@ -7,6 +7,14 @@ from typing import Optional
import jwt import jwt
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "dev-jwt-secret-change-in-production") 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" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 24 * 30 # 30 days ACCESS_TOKEN_EXPIRE_HOURS = 24 * 30 # 30 days

View file

@ -89,10 +89,12 @@ async def google_callback(request: Request):
jwt_token = create_access_token(user["id"], user["email"], user["role"]) jwt_token = create_access_token(user["id"], user["email"], user["role"])
# Redirect to dashboard with token in cookie # Redirect to dashboard with token in cookie
is_https = request.url.scheme == "https"
response = RedirectResponse(url="/dashboard", status_code=302) response = RedirectResponse(url="/dashboard", status_code=302)
response.set_cookie( response.set_cookie(
key="access_token", value=jwt_token, key="access_token", value=jwt_token,
httponly=True, max_age=86400 * 30, samesite="lax", httponly=True, max_age=86400 * 30, samesite="lax",
secure=is_https,
) )
return response return response

View file

@ -68,13 +68,9 @@ async def password_setup(
raise HTTPException(status_code=400, detail="Invalid setup token") raise HTTPException(status_code=400, detail="Invalid setup token")
# Hash and save password # Hash and save password
try: from argon2 import PasswordHasher
from argon2 import PasswordHasher ph = PasswordHasher()
ph = PasswordHasher() hashed = ph.hash(request.password)
hashed = ph.hash(request.password)
except ImportError:
import hashlib
hashed = hashlib.sha256(request.password.encode()).hexdigest()
repo.update(id=user["id"], password_hash=hashed, setup_token=None) repo.update(id=user["id"], password_hash=hashed, setup_token=None)
token = create_access_token(user["id"], user["email"], user["role"]) token = create_access_token(user["id"], user["email"], user["role"])

View file

@ -88,12 +88,8 @@ async def bootstrap(
user_id = str(uuid.uuid4()) user_id = str(uuid.uuid4())
password_hash = None password_hash = None
if request.password: if request.password:
try: from argon2 import PasswordHasher
from argon2 import PasswordHasher password_hash = PasswordHasher().hash(request.password)
password_hash = PasswordHasher().hash(request.password)
except ImportError:
import hashlib
password_hash = hashlib.sha256(request.password.encode()).hexdigest()
repo.create( repo.create(
id=user_id, id=user_id,

View file

@ -39,15 +39,15 @@ def create_app() -> FastAPI:
) )
# Session middleware (required for OAuth state) # Session middleware (required for OAuth state)
app.add_middleware( import secrets as _secrets
SessionMiddleware, session_secret = os.environ.get("SESSION_SECRET", os.environ.get("JWT_SECRET_KEY", _secrets.token_hex(32)))
secret_key=os.environ.get("JWT_SECRET_KEY", "dev-session-secret"), app.add_middleware(SessionMiddleware, secret_key=session_secret)
)
# CORS for CLI and external clients # CORS for CLI and external clients
cors_origins = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:8000").split(",")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=[o.strip() for o in cors_origins],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],

View file

@ -16,6 +16,8 @@ dependencies = [
# Authentication # Authentication
"PyJWT>=2.8.0", "PyJWT>=2.8.0",
"itsdangerous>=2.1.0", "itsdangerous>=2.1.0",
"authlib>=1.3.0",
"argon2-cffi>=23.1.0",
# HTTP client # HTTP client
"httpx>=0.27.0", "httpx>=0.27.0",
# CLI # CLI