diff --git a/api/src/core/auth.py b/api/src/core/auth.py index fe97015..19af572 100644 --- a/api/src/core/auth.py +++ b/api/src/core/auth.py @@ -36,6 +36,8 @@ JWKS_ALGORITHMS: tuple[str, ...] = ("RS256", "ES256") DEV_ALGORITHM = "HS256" # Допуск на расхождение часов с эмитентом токена JWKS_LEEWAY_SECONDS = 10 +# Минимальная длина dev-секрета HS256 (RFC 7518 §3.2: не короче выхода SHA-256) +DEV_SECRET_MIN_BYTES = 32 # Срок жизни dev-токена (требование спеки authn-authz) DEV_TOKEN_TTL = timedelta(hours=8) # Время жизни кэша набора ключей JWKS (PyJWKClient), секунды. Кэш отдельных ключей по kid (cache_keys) выключен @@ -134,6 +136,9 @@ class DevHs256Verifier: def __init__(self, secret: str, *, email_claim: str = "email") -> None: if not secret: raise ValueError("секрет dev-эмитента не задан") + if len(secret.encode()) < DEV_SECRET_MIN_BYTES: + # HS256 по RFC 7518 §3.2 требует ключ не короче хэша (32 байта); PyJWT иначе шумит предупреждением + raise RuntimeError(f"AUTH_DEV_SECRET короче {DEV_SECRET_MIN_BYTES} байт") self._secret = secret self._email_claim = email_claim diff --git a/api/tests/test_auth_core.py b/api/tests/test_auth_core.py index 2e98a96..5df00f9 100644 --- a/api/tests/test_auth_core.py +++ b/api/tests/test_auth_core.py @@ -210,6 +210,17 @@ async def test_dev_issue_token_custom_ttl_and_claim() -> None: assert claims.exp - datetime.now(UTC) < timedelta(minutes=11) +def test_dev_short_secret_forbidden() -> None: + """T16.7: секрет короче 32 байт отвергается (RuntimeError), 32 байта — допустимы; предупреждений PyJWT нет.""" + with pytest.raises(RuntimeError, match="AUTH_DEV_SECRET короче 32 байт"): + DevHs256Verifier("x" * 31) + # «байт», а не символов: 16 кириллических символов = 32 байта в UTF-8 + DevHs256Verifier("я" * 16) + with pytest.raises(RuntimeError, match="короче 32 байт"): + DevHs256Verifier("я" * 15) + DevHs256Verifier("x" * 32) + + def test_dev_empty_secret_forbidden() -> None: with pytest.raises(ValueError, match="секрет"): DevHs256Verifier("") @@ -635,6 +646,12 @@ def test_factory_dev_wins_over_jwks_when_both_flags( assert isinstance(build_verifier(s), DevHs256Verifier) +def test_factory_dev_short_secret_is_config_error(monkeypatch: pytest.MonkeyPatch) -> None: + s = _settings(monkeypatch, AUTH_DEV_ISSUER=True, AUTH_DEV_SECRET="short-secret", DEBUG=True) + with pytest.raises(RuntimeError, match="AUTH_DEV_SECRET короче 32 байт"): + build_verifier(s) + + def test_factory_dev_flags_without_secret_is_config_error(monkeypatch: pytest.MonkeyPatch) -> None: s = _settings(monkeypatch, AUTH_DEV_ISSUER=True, DEBUG=True) with pytest.raises(RuntimeError, match="AUTH_DEV_SECRET"):