Перейти к основному содержимому

0003. Хеш пароля и стратегия refresh-токенов

Копия ADR из репозитория: backend/docs/adr/0003-password-hash-and-refresh-rotation.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-05-29
  • Связанные задачи: OP-28 (F2 · Auth)

Контекст и проблема​

OP-28 закладывает базовую аутентификацию: регистрация, вход, refresh, logout. Два рано фиксируемых решения определят безопасность всех будущих auth-зависимых модулей:

  1. Алгоритм хеширования паролей at-rest.
  2. Жизненный цикл refresh-токенов: одноразовый/ротация/отзыв.

Рассмотренные варианты​

Хеш пароля​

  1. bcrypt — де-факто стандарт, широкая поддержка, давно проверен. Нативная зависимость (node-gyp), не учитывает GPU-стойкость, фиксированная стоимость только по CPU.
  2. scrypt — встроенный в Node.js (crypto.scrypt), без сторонних бинарников. Менее популярен в новых проектах, опции по умолчанию не оптимальны под текущее железо.
  3. argon2id — победитель Password Hashing Competition (2015), рекомендован OWASP с 2024 г. Учитывает память (memory-hard) — устойчив к GPU/ASIC атакам. Зависимость argon2 использует prebuilt-биндинги (без node-gyp при наличии платформы в matrix), на macOS/linux/win устанавливается из коробки.

Refresh-токены​

  1. Stateless JWT с длинным TTL, без отзыва — простота, но потеря refresh = потеря контроля до истечения. Невозможно «выйти со всех устройств».
  2. JWT + чёрный список (jti) в Redis — отзыв есть, но требует Redis-доступа на каждой проверке refresh. Большой blacklist при массовых ротациях.
  3. Opaque refresh-токен, хеш в БД, ротация при каждом use — токен в БД (хеш через argon2/SHA-256), при /auth/refresh старый помечается revoked_at, выдаётся новый. «Выход со всех устройств» = revoke всех refresh-токенов пользователя. Compromised-detection при reuse отозванного.

Решение​

Пароль — argon2id​

  • Параметры: memoryCost: 2^16 (64 MiB), timeCost: 3, parallelism: 1, type: argon2id (значения из OWASP Password Storage Cheat Sheet, 2024).
  • Зависимость argon2 (v0.44+) — prebuilt-биндинги для всех платформ CI/CD.
  • Soft-migration на bcrypt НЕ предусмотрена (проект новый, нет легаси-хешей).

Refresh — rotate с отзывом в PostgreSQL​

  • Refresh-токен — opaque random (32 байта, base64url), хеш хранится в refresh_tokens (sha-256, без соли — токен сам random, коллизии исключены).
  • Поля: id, user_id, token_hash unique, expires_at, revoked_at NULL, created_at. Индексы: user_id, token_hash.
  • /auth/refresh: проверяем token_hash + revoked_at IS NULL + expires_at > now(). При успехе — revoked_at = now() старому, выдаём новый.
  • /auth/logout: revoked_at = now() текущему refresh-токену.
  • «Выход со всех устройств» — отдельная Story в будущем (revoke по user_id).
  • TTL по умолчанию: access 15m, refresh 30d (env: JWT_ACCESS_TTL, JWT_REFRESH_TTL).

Access — JWT в стиле stateless​

  • HS256, payload { sub: userId, jti: uuid }, без отзыва на per-request пути (валидируется по подписи + exp). Окно компрометации — TTL access.

Последствия​

  • Положительные:
    • argon2id — современный baseline; не нужно повышать «cost factor» в ближайшие годы под рост железа.
    • Refresh в БД даёт контроль: revoke, аудит, reuse-detection (если повторно использован отозванный — алерт, отозвать все пользователю).
    • Stateless access — нет нагрузки на БД/Redis на горячем пути.
  • Отрицательные / компромиссы:
    • Доступ к argon2 требует prebuilt-биндингов или нативной сборки; в alpine Docker может потребоваться python3 make g++ на этапе билда.
    • Каждый /auth/refresh — write в БД (revoke + insert). Не горячий путь, ок.
  • Дальше (вне scope OP-28):
    • OP-29 (RBAC) — встанет поверх UserEntity из этого тикета.
    • «Выход со всех устройств» — отдельная Story (revoke by user_id).
    • Email-верификация, reset password — отдельные Story.
    • Rate-limit на /auth/login — отдельный Task (под F8 или F2).