Fix cross-origin API access, bcrypt login break, and weak secret key
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / docker-build (push) Has been cancelled
Test / backend-test (push) Has been cancelled
Test / frontend-test (push) Has been cancelled
Test / security-scan (push) Has been cancelled

The frontend only worked from localhost:3000 — the browser made absolute
cross-origin calls to localhost:8000, which (a) points at the visitor's own
machine when accessed via the LAN IP or Cloudflare tunnel, and (b) was blocked
by CORS (backend only allowed localhost:3000). Now all API calls are
same-origin and proxied to the backend:

- api.ts: default API base to same-origin ("") instead of localhost:8000
- docker-compose: NEXT_PUBLIC_API_URL="" (client uses relative /api)
- next.config.ts: server-side rewrite uses API_INTERNAL_URL (http://backend:8000
  inside Docker) so :3000 direct access proxies correctly
- main.py: broaden CORS via allow_origin_regex (localhost + private LAN) and an
  optional CORS_ORIGINS env var, as defense-in-depth for direct :8000 access

Login was returning 500: passlib 1.7.4 is incompatible with the bcrypt 5.0.0
actually installed in the image (requirements pin 4.1.2, but the image drifted).
- security.py: call bcrypt directly, truncating to 72 bytes; existing $2b$
  hashes verify unchanged, and it works under both bcrypt 4.x and 5.x

Security: SECRET_KEY was a known placeholder string while the app is exposed on
the LAN and via Cloudflare tunnel — anyone could forge admin JWTs. Regenerated
to a strong random value in backend/.env (gitignored; not committed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-07-07 15:13:23 +00:00
parent 2bdc085bc3
commit c44fa6fcda
6 changed files with 40 additions and 8 deletions

View File

@@ -1,21 +1,34 @@
from datetime import datetime, timedelta
from typing import Optional, Any
import bcrypt
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
# We call bcrypt directly rather than through passlib: passlib 1.7.x is
# incompatible with bcrypt >= 4.1 (its version shim raises on the modern
# library). bcrypt only uses the first 72 bytes of a password, so we truncate
# to that to avoid the ValueError bcrypt 5.x raises on longer inputs. Existing
# $2b$ hashes (created via passlib's bcrypt backend) verify unchanged.
_BCRYPT_MAX_BYTES = 72
def _to_bytes(password: str) -> bytes:
return password.encode("utf-8")[:_BCRYPT_MAX_BYTES]
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
try:
return bcrypt.checkpw(_to_bytes(plain), hashed.encode("utf-8"))
except (ValueError, TypeError):
return False
def hash_password(password: str) -> str:
return pwd_context.hash(password)
return bcrypt.hashpw(_to_bytes(password), bcrypt.gensalt()).decode("utf-8")
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: