diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..2746210 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-08-31 - JWT Secret Hardcoded Fallback +**Vulnerability:** JWT authentication fallback key `'jwt-secret'` was hardcoded when `JWT_SECRET_KEY` env var was missing, permitting token forgery. +**Learning:** Default fallbacks for secret credentials in utility classes can lead to accidental deployment with known default keys. +**Prevention:** Fail fast by raising exceptions (or returning validation failure) when required cryptographic secrets are missing from configuration. diff --git a/auth/utils.py b/auth/utils.py index 4bded89..04d3830 100644 --- a/auth/utils.py +++ b/auth/utils.py @@ -37,6 +37,11 @@ class JWTUtils: @staticmethod def create_tokens(user_id: str, username: str) -> Tuple[str, str]: """Create access and refresh tokens""" + secret_key = os.getenv('JWT_SECRET_KEY') + if not secret_key: + # SECURITY: Do not use default/hardcoded fallback secret key + raise RuntimeError("JWT_SECRET_KEY environment variable is not configured.") + access_token = jwt.encode( { 'user_id': user_id, @@ -44,7 +49,7 @@ def create_tokens(user_id: str, username: str) -> Tuple[str, str]: 'exp': datetime.utcnow() + timedelta(hours=1), 'type': 'access' }, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithm='HS256' ) @@ -55,7 +60,7 @@ def create_tokens(user_id: str, username: str) -> Tuple[str, str]: 'exp': datetime.utcnow() + timedelta(days=30), 'type': 'refresh' }, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithm='HS256' ) @@ -64,10 +69,15 @@ def create_tokens(user_id: str, username: str) -> Tuple[str, str]: @staticmethod def decode_token(token: str) -> Optional[dict]: """Decode and verify token""" + secret_key = os.getenv('JWT_SECRET_KEY') + if not secret_key: + # SECURITY: Do not verify tokens with a fallback secret key + return None + try: payload = jwt.decode( token, - os.getenv('JWT_SECRET_KEY', 'jwt-secret'), + secret_key, algorithms=['HS256'] ) return payload diff --git a/tests/test_auth_utils.py b/tests/test_auth_utils.py new file mode 100644 index 0000000..fc5b2f2 --- /dev/null +++ b/tests/test_auth_utils.py @@ -0,0 +1,45 @@ +import os +import pytest +import sys + +# Ensure auth module can be imported +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from unittest.mock import MagicMock + +# Mock cache_db module dependencies if not installed +sys.modules.setdefault("cache_db", MagicMock()) +sys.modules.setdefault("cache_db.redis_client", MagicMock()) +sys.modules.setdefault("cache_db.models", MagicMock()) + +from auth.utils import JWTUtils, PasswordUtils + + +def test_password_hashing(): + pwd = "securepassword123" + hashed = PasswordUtils.hash_password(pwd) + assert PasswordUtils.verify_password(pwd, hashed) is True + assert PasswordUtils.verify_password("wrongpassword", hashed) is False + + +def test_jwt_utils_requires_secret_key(monkeypatch): + monkeypatch.delenv("JWT_SECRET_KEY", raising=False) + + with pytest.raises(RuntimeError, match="JWT_SECRET_KEY environment variable is not configured"): + JWTUtils.create_tokens("user123", "alice") + + assert JWTUtils.decode_token("dummy.token.string") is None + + +def test_jwt_utils_with_secret_key(monkeypatch): + monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-12345") + + access_token, refresh_token = JWTUtils.create_tokens("user123", "alice") + assert access_token is not None + assert refresh_token is not None + + decoded = JWTUtils.decode_token(access_token) + assert decoded is not None + assert decoded.get("user_id") == "user123" + assert decoded.get("username") == "alice" + assert decoded.get("type") == "access"