fix(auth): проверка пароля больше не блокирует весь backend
На нагрузочном тесте 31.07.2026 около 70 человек заходили одновременно. Вход развалился: p95 `/api/v1/auth/token` — 7.28 с, p95 `guest-join` — 7.06 с, в БД 33 соединения `idle in transaction` при ОДНОМ активном запросе. Люди попадали внутрь с пятой-десятой попытки, часть не попала вовсе. Медиа при этом работало штатно: 30 участников с 27 камерами в следующем окне прошли без единого лага. Причина — argon2 считался синхронно внутри async-обработчика. Замер на боевом сервере: 95–155 мс на одну проверку, и всё это время event loop процесса стоит целиком. Транзакция БД к тому моменту уже открыта (`get_by_email` сделал SELECT), поэтому соединение висело без работы, пул из 40 выбирался, и отказы получали совершенно посторонние ручки — включая вход в конференцию, где никакого пароля не проверялось. Что изменилось: - `hash_password`/`verify_password` стали асинхронными и считаются в пуле потоков (`asyncio.to_thread`). argon2-cffi освобождает GIL, поэтому проверки идут по-настоящему параллельно; - параметры argon2id заменены с дефолтов библиотеки (t=3, m=64 МБ, p=4) на рекомендацию OWASP (t=2, m=19 МБ, p=1): 95 мс → 42 мс. Отдельно важен `parallelism`: при p=4 одна проверка пароля занимала все четыре ядра сервера — те же, на которых работает LiveKit; - добавлен `needs_rehash`: существующие хэши проверяются как прежде (параметры зашиты в саму строку) и лениво перевыпускаются при первом успешном входе. Расчёт по замерам: пачка из 70 логинов — 6.7–10.9 с блокировки против ~0.36 с без неё. Тесты: event loop продолжает тикать во время проверки; 8 параллельных проверок укладываются заметно быстрее восьми последовательных; хэш со старыми параметрами принимается и перевыпускается при входе.
This commit is contained in:
@@ -9,6 +9,7 @@ from typing import Annotated
|
||||
|
||||
import httpx
|
||||
import pytest_asyncio
|
||||
from argon2 import PasswordHasher
|
||||
from fastapi import Depends, FastAPI
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -16,6 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from api.auth import get_auth_service
|
||||
from core.db import get_session
|
||||
from core.redis import redis_client
|
||||
from core.security import needs_rehash
|
||||
from models.team import Team
|
||||
from models.user import User
|
||||
from services.auth import AuthService
|
||||
@@ -491,3 +493,35 @@ async def test_register_no_reply_to_when_contact_email_disabled(
|
||||
|
||||
assert response.status_code == 201, response.text
|
||||
assert email_backend.reply_to[-1] is None
|
||||
|
||||
|
||||
async def test_login_rehashes_legacy_password(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
|
||||
) -> None:
|
||||
"""Вход с паролем, захэшированным старыми параметрами, перевыпускает хэш.
|
||||
|
||||
Параметры argon2 зашиты в саму строку хэша, поэтому смена настроек
|
||||
(0.0.17: дефолты библиотеки → рекомендации OWASP) сама по себе не ускоряет
|
||||
проверку уже существующих паролей. Миграция идёт лениво — при первом
|
||||
успешном входе, когда открытый пароль есть на руках.
|
||||
"""
|
||||
email = "legacy-hash@example.com"
|
||||
password = "supersecret1"
|
||||
await _register_and_verify(client, email_backend, email=email, password=password)
|
||||
|
||||
# Подменяем хэш на выданный прежними параметрами (t=3, m=64 МБ, p=4).
|
||||
legacy_hash = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4).hash(password)
|
||||
user = await db_session.scalar(select(User).where(User.email == email))
|
||||
assert user is not None
|
||||
user.password_hash = legacy_hash
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/auth/token", data={"username": email, "password": password}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
await db_session.refresh(user)
|
||||
assert user.password_hash != legacy_hash, "старый хэш не был перевыпущен"
|
||||
assert "m=19456" in user.password_hash
|
||||
assert needs_rehash(user.password_hash) is False
|
||||
|
||||
Reference in New Issue
Block a user