feat: dev-секрет HS256 короче 32 байт отвергается (T16.7)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
953d978cc7
commit
cd64087b0d
@ -36,6 +36,8 @@ JWKS_ALGORITHMS: tuple[str, ...] = ("RS256", "ES256")
|
|||||||
DEV_ALGORITHM = "HS256"
|
DEV_ALGORITHM = "HS256"
|
||||||
# Допуск на расхождение часов с эмитентом токена
|
# Допуск на расхождение часов с эмитентом токена
|
||||||
JWKS_LEEWAY_SECONDS = 10
|
JWKS_LEEWAY_SECONDS = 10
|
||||||
|
# Минимальная длина dev-секрета HS256 (RFC 7518 §3.2: не короче выхода SHA-256)
|
||||||
|
DEV_SECRET_MIN_BYTES = 32
|
||||||
# Срок жизни dev-токена (требование спеки authn-authz)
|
# Срок жизни dev-токена (требование спеки authn-authz)
|
||||||
DEV_TOKEN_TTL = timedelta(hours=8)
|
DEV_TOKEN_TTL = timedelta(hours=8)
|
||||||
# Время жизни кэша набора ключей JWKS (PyJWKClient), секунды. Кэш отдельных ключей по kid (cache_keys) выключен
|
# Время жизни кэша набора ключей JWKS (PyJWKClient), секунды. Кэш отдельных ключей по kid (cache_keys) выключен
|
||||||
@ -134,6 +136,9 @@ class DevHs256Verifier:
|
|||||||
def __init__(self, secret: str, *, email_claim: str = "email") -> None:
|
def __init__(self, secret: str, *, email_claim: str = "email") -> None:
|
||||||
if not secret:
|
if not secret:
|
||||||
raise ValueError("секрет dev-эмитента не задан")
|
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._secret = secret
|
||||||
self._email_claim = email_claim
|
self._email_claim = email_claim
|
||||||
|
|
||||||
|
|||||||
@ -210,6 +210,17 @@ async def test_dev_issue_token_custom_ttl_and_claim() -> None:
|
|||||||
assert claims.exp - datetime.now(UTC) < timedelta(minutes=11)
|
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:
|
def test_dev_empty_secret_forbidden() -> None:
|
||||||
with pytest.raises(ValueError, match="секрет"):
|
with pytest.raises(ValueError, match="секрет"):
|
||||||
DevHs256Verifier("")
|
DevHs256Verifier("")
|
||||||
@ -635,6 +646,12 @@ def test_factory_dev_wins_over_jwks_when_both_flags(
|
|||||||
assert isinstance(build_verifier(s), DevHs256Verifier)
|
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:
|
def test_factory_dev_flags_without_secret_is_config_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
s = _settings(monkeypatch, AUTH_DEV_ISSUER=True, DEBUG=True)
|
s = _settings(monkeypatch, AUTH_DEV_ISSUER=True, DEBUG=True)
|
||||||
with pytest.raises(RuntimeError, match="AUTH_DEV_SECRET"):
|
with pytest.raises(RuntimeError, match="AUTH_DEV_SECRET"):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user