feat(api): sign-in, refresh rotation and bootstrap admin (M1)
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m15s
ci / api / api (push) Successful in 1m31s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 39s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 26s

- users, refresh_tokens and audit_log schema (migration 00002)
- argon2id passwords with rehash on login, password policy, lockout
  with exponential back-off and a per-IP rate limit in Redis
- Ed25519 JWT access tokens with kid-based key rotation; rotating
  refresh tokens with reuse detection (cookie for web, body for Android)
- bootstrap admin on first start; must_change_credentials and
  must_change_password gate every other endpoint
- /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password,
  /me/bootstrap-credentials, /me/sessions; server-info reports
  bootstrap_required
- role policy from docs/auth-and-roles.md with table tests
- problem+json errors with field errors; ru/en translations of the codes
- install.sh and update.sh generate JWT_PRIVATE_KEY

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 06:12:25 +03:00
1 parent b6def39e29
commit ed4a492dba
37 files changed
+5826 -49

No files matched your search

+9 -3
View File
@@ -55,7 +55,7 @@
- argon2id: `m=64 MiB, t=3, p=2`, соль 16 байт; параметры в хэше (PHC), при изменении — rehash при входе.
- Политика: ≥ 8 символов для `user`, ≥ 12 для `moderator`/`admin`; не совпадает с логином; проверка по
встроенному списку 10k самых частых паролей.
встроенному списку частых паролей (`internal/auth/common_passwords.txt`; пока ~150 записей, цель — 10k).
- Временный пароль при создании пользователя и при сбросе генерирует сервер: 12 символов, показывается **один раз**;
у пользователя ставится `must_change_password = true`.
- Защита от перебора: после 5 неудачных попыток — `locked_until = now() + 5 мин` (растёт экспоненциально до 1 ч),
@@ -73,10 +73,16 @@
| Android | в памяти | `expo-secure-store` |
- **Ротация**: каждый `/auth/refresh` помечает старый токен `used_at` и выдаёт новый в той же `family_id`.
Повторное предъявление уже использованного токена ⇒ компрометация ⇒ отзыв всей семьи.
Повторное предъявление уже использованного токена ⇒ компрометация ⇒ отзыв всей семьи
(`401 auth.refresh_reused`). Исключение — первые 10 с после ротации: так бывает, когда две вкладки
обновляют токен одновременно; ответ `401 auth.refresh_race` без отзыва, клиент повторяет запрос
с уже новой cookie.
- **Мгновенный отзыв** (блокировка, смена роли, смена пароля): у пользователя инкрементируется `token_version`
(в Redis-кэше), middleware сравнивает с `ver` в access-токене.
- Ключи подписи: `JWT_PRIVATE_KEY` из env / файла; поддержка `kid` для ротации ключей.
- Ключи подписи: `JWT_PRIVATE_KEY` (base64 seed Ed25519) из env; `kid` = первые 8 байт SHA-256 публичного ключа,
прежние ключи для проверки — `JWT_PREVIOUS_KEYS`.
- Смена пароля и учётных данных отзывает все сессии и сразу выдаёт новую пару текущему устройству —
пользователь остаётся в системе, остальные устройства разлогиниваются.
- CSRF: refresh-cookie `SameSite=Strict` и путь только `/api/v1/auth`; остальные запросы — Bearer-заголовок,
поэтому CSRF к ним неприменим.
+1
View File
@@ -38,6 +38,7 @@ CREATE TABLE refresh_tokens (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
family_id uuid NOT NULL, -- цепочка ротаций одного входа
family_started_at timestamptz NOT NULL, -- время входа: абсолютный срок семьи (180 дней)
token_hash bytea NOT NULL UNIQUE, -- SHA-256 от токена
device_name text, -- "Chrome / Windows", "Pixel 8"
client_type text NOT NULL, -- 'web' | 'android'
+4 -1
View File
@@ -67,7 +67,10 @@
| `DATABASE_URL` | `postgres://pipu:…@postgres:5432/pipu` | |
| `REDIS_URL` | `redis://redis:6379/0` | |
| `MEILI_URL`, `MEILI_MASTER_KEY` | | |
| `JWT_PRIVATE_KEY_FILE` | `/run/secrets/jwt_ed25519.pem` | Генерируется `install.sh` |
| `JWT_PRIVATE_KEY` | base64 от 32 байт (seed Ed25519) | Подпись access-токенов. Создают `install.sh` / `update.sh`, если его нет; пусто — случайный ключ до перезапуска |
| `JWT_PREVIOUS_KEYS` | пусто | Прежние ключи через запятую — токены, подписанные ими, ещё принимаются (ротация) |
| `ACCESS_TOKEN_TTL` | `15m` | Срок access-токена |
| `COOKIE_SECURE` | `true` | Флаг `Secure` у refresh-cookie; `false` — только для разработки по http не на localhost |
| `STREAM_SIGNING_KEY` | 32 байта base64 | HMAC подписанных ссылок |
| `INTERNAL_TOKEN` | случайный | Межсервисная аутентификация |
| `BOOTSTRAP_ADMIN_USERNAME` | `admin` | |