fix: raise RuntimeError on missing JWT_SECRET_KEY in non-test environments
Prevents production deployments from silently using a hardcoded default secret. TESTING=1 still resolves to a built-in test key so the existing test suite is unaffected. Adds a test that verifies the RuntimeError is raised when neither JWT_SECRET_KEY nor TESTING is set.
This commit is contained in:
parent
0d3ab5060c
commit
4aa97c23d2
2 changed files with 43 additions and 4 deletions
|
|
@ -6,12 +6,20 @@ 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", "")
|
||||||
|
|
||||||
import warnings as _warnings
|
if not SECRET_KEY:
|
||||||
if len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", "true"):
|
if os.environ.get("TESTING", "").lower() in ("1", "true"):
|
||||||
|
SECRET_KEY = "test-jwt-secret-key-minimum-32-chars!!"
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"JWT_SECRET_KEY environment variable is required. "
|
||||||
|
"Generate one: python -c \"import secrets; print(secrets.token_hex(32))\""
|
||||||
|
)
|
||||||
|
elif len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1", "true"):
|
||||||
|
import warnings as _warnings
|
||||||
_warnings.warn(
|
_warnings.warn(
|
||||||
f"JWT_SECRET_KEY is {len(SECRET_KEY)} chars — minimum 32 recommended for production",
|
f"JWT_SECRET_KEY is {len(SECRET_KEY)} chars — minimum 32 recommended",
|
||||||
UserWarning, stacklevel=2,
|
UserWarning, stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Security tests — sandbox escapes, SQL injection, access control."""
|
"""Security tests — sandbox escapes, SQL injection, access control."""
|
||||||
|
|
||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
@ -190,3 +192,32 @@ class TestAuthSecurity:
|
||||||
c, _ = client
|
c, _ = client
|
||||||
resp = c.get("/api/scripts")
|
resp = c.get("/api/scripts")
|
||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
# ---- JWT Secret Hardening ----
|
||||||
|
|
||||||
|
class TestJwtSecretHardening:
|
||||||
|
def test_raises_without_jwt_secret_in_non_test_env(self):
|
||||||
|
"""Module-level code must raise RuntimeError when JWT_SECRET_KEY is absent
|
||||||
|
and TESTING is not set, preventing accidental production deploys with no secret."""
|
||||||
|
saved_key = os.environ.pop("JWT_SECRET_KEY", None)
|
||||||
|
saved_testing = os.environ.pop("TESTING", None)
|
||||||
|
# Eject any cached module so the re-import re-executes module-level code
|
||||||
|
sys.modules.pop("app.auth.jwt", None)
|
||||||
|
try:
|
||||||
|
with pytest.raises(RuntimeError, match="JWT_SECRET_KEY environment variable is required"):
|
||||||
|
importlib.import_module("app.auth.jwt")
|
||||||
|
finally:
|
||||||
|
# Restore environment before re-importing so the module loads cleanly
|
||||||
|
if saved_key is not None:
|
||||||
|
os.environ["JWT_SECRET_KEY"] = saved_key
|
||||||
|
if saved_testing is not None:
|
||||||
|
os.environ["TESTING"] = saved_testing
|
||||||
|
# If neither was set (bare test run), use TESTING flag so reload works
|
||||||
|
if saved_key is None and saved_testing is None:
|
||||||
|
os.environ["TESTING"] = "1"
|
||||||
|
sys.modules.pop("app.auth.jwt", None)
|
||||||
|
importlib.import_module("app.auth.jwt")
|
||||||
|
# Clean up the temporary TESTING flag if we added it
|
||||||
|
if saved_key is None and saved_testing is None:
|
||||||
|
os.environ.pop("TESTING", None)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue