Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 13 additions & 3 deletions auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@ 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,
'username': username,
'exp': datetime.utcnow() + timedelta(hours=1),
'type': 'access'
},
os.getenv('JWT_SECRET_KEY', 'jwt-secret'),
secret_key,
algorithm='HS256'
)

Expand All @@ -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'
)

Expand All @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/test_auth_utils.py
Original file line number Diff line number Diff line change
@@ -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"