Harden server restart: remove import-time crashes, centralise secret validation

- auth_service.py: RESET_OTP_SECRET no longer crashes at import; read lazily
  inside _hash_otp() so the module always loads cleanly
- main.py: _validate_runtime_secrets() now checks both BROKER_TOKEN_KEY and
  RESET_OTP_SECRET together, reporting all missing vars in one clear message
- .env.example: documents every required/optional env var with generation commands

With load_dotenv() + .env file, all secrets survive pm2 restarts automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thigazhezhilan J 2026-05-03 12:58:22 +05:30
parent 0a7e038be9
commit ae3a335ea1
3 changed files with 57 additions and 11 deletions

31
backend/.env.example Normal file
View File

@ -0,0 +1,31 @@
# ── Required secrets (server will NOT start without these in production) ──────
# 32-byte URL-safe base64 Fernet key for encrypting Zerodha access tokens.
# Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
BROKER_TOKEN_KEY=
# Secret used to HMAC-sign password-reset OTPs. Any long random string works.
# Generate: python -c "import secrets; print(secrets.token_hex(32))"
RESET_OTP_SECRET=
# ── Environment ───────────────────────────────────────────────────────────────
APP_ENV=production
CORS_ORIGINS=https://quantfortune.com,https://app.quantfortune.com
# ── Database ──────────────────────────────────────────────────────────────────
DATABASE_URL=postgresql://user:password@localhost:5432/quantfortune
# ── Email (optional — auto-login OTP emails) ─────────────────────────────────
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
# ── Zerodha auto-login (optional) ────────────────────────────────────────────
# ZERODHA_API_KEY=
# ZERODHA_API_SECRET=
# ── Tuning (all have safe defaults, only set if you need to override) ─────────
# SESSION_TTL_SECONDS=604800
# RESET_OTP_TTL_MINUTES=10
# LIVE_EQUITY_SNAPSHOT_HOUR=15

View File

@ -108,16 +108,29 @@ def _validate_runtime_secrets():
env_name = _environment_name()
if env_name not in PRODUCTION_ENV_NAMES:
return
missing = []
broker_token_key = (os.getenv("BROKER_TOKEN_KEY") or "").strip()
if not broker_token_key:
raise RuntimeError("BROKER_TOKEN_KEY must be configured in production")
missing.append("BROKER_TOKEN_KEY")
else:
try:
from cryptography.fernet import Fernet
Fernet(broker_token_key.encode("utf-8"))
except Exception:
raise RuntimeError(
"BROKER_TOKEN_KEY is set but invalid — must be a 32-byte URL-safe base64 key. "
"Generate one with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
"Generate: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
)
if not (os.getenv("RESET_OTP_SECRET") or "").strip():
missing.append("RESET_OTP_SECRET")
if missing:
raise RuntimeError(
f"Missing required environment variables: {', '.join(missing)}. "
"Add them to the .env file in the backend directory."
)
if (os.getenv("ENABLE_SUPER_ADMIN_BOOTSTRAP") or "").strip() in {"1", "true", "yes"}:
if not (os.getenv("SUPER_ADMIN_EMAIL") or "").strip():

View File

@ -17,9 +17,11 @@ SESSION_REFRESH_WINDOW_SECONDS = int(
RESET_OTP_TTL_MINUTES = int(os.getenv("RESET_OTP_TTL_MINUTES", "10"))
PASSWORD_HASHER = PasswordHasher()
LEGACY_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
RESET_OTP_SECRET = (os.getenv("RESET_OTP_SECRET") or "").strip()
if not RESET_OTP_SECRET:
raise RuntimeError("RESET_OTP_SECRET must be configured")
def _get_reset_otp_secret() -> str:
secret = (os.getenv("RESET_OTP_SECRET") or "").strip()
if not secret:
raise RuntimeError("RESET_OTP_SECRET is not configured on this server")
return secret
def _now_utc() -> datetime:
@ -43,7 +45,7 @@ def _is_legacy_password_hash(password_hash: str | None) -> bool:
def _hash_otp(email: str, otp: str) -> str:
payload = f"{email}:{otp}:{RESET_OTP_SECRET}"
payload = f"{email}:{otp}:{_get_reset_otp_secret()}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()