feat(auth): шаг 0.6 — TTL access 15 минут + хэширование refresh/reset-токенов в БД
Some checks are pending
Branch Check / Type Check & Build (push) Waiting to run
Some checks are pending
Branch Check / Type Check & Build (push) Waiting to run
- server/utils/jwt.ts: дефолт JWT_ACCESS_EXPIRES 30d → 15m (env сохранено), hashToken() (sha256 hex) - migrations/0082_token_hashes.sql: *_hash колонки в user_sessions/users, DELETE FROM user_sessions (глобальный разлогин), legacy plain-колонки сохранены, но не пишутся - storage: lookup/отзыв сессий и reset-токенов по хэшу; markSessionReplaced по id сессии - auth.service: ротация в rotateFamilySession(), grace-period ротирует активную сессию вместо возврата plain-токена - auth.core.routes: forgot/reset-password пишут/ищут sha256-хэш, plain только в письме - tests/token-security.test.ts: 12 тестов (выпуск по хэшу, ротация, reuse detection, grace, reset) - .env.example, swagger, IMPLEMENTATION_LOG.md обновлены Проверки: npm run check чисто, vitest 118/118, lint 0 errors, build собирается
This commit is contained in:
@@ -442,3 +442,26 @@
|
||||
- Как проверялось: на проде подтверждено включение (pg_class relrowsecurity+relforcerowsecurity=31), smoke через API; локально — полный цикл на копии прод-БД.
|
||||
- Влияние на поиск/UX: нет.
|
||||
- Подводные камни: при переносе на новый сервер — сначала ENABLE_RLS=true в .env, иначе app не поднимется (это и есть цель шага).
|
||||
|
||||
---
|
||||
|
||||
## [0.6] TTL access-токена 15 минут + хэширование refresh/reset-токенов в БД
|
||||
|
||||
- Статус: ⏸ частично — код готов, ждёт окно деплоя (разлогин всех)
|
||||
- Зачем: украденный access-токен жил до 30 дней; refresh/reset-токены лежали в БД в открытом виде — при утечке БД атакующий получал вечный доступ (аудит, Фаза 0).
|
||||
- Что изменено:
|
||||
- `server/utils/jwt.ts` — дефолт `JWT_ACCESS_EXPIRES` '30d' → '15m' (env-переопределение сохранено), добавлен `hashToken()` (sha256 hex) и экспорт `ACCESS_TOKEN_EXPIRY`.
|
||||
- Миграция `migrations/0082_token_hashes.sql` — колонки `refresh_token_hash` (NOT NULL, UNIQUE), `parent_refresh_token_hash`, `replaced_by_token_hash` в `user_sessions`; `reset_password_token_hash` в `users`. **`DELETE FROM user_sessions` + очистка reset-токенов — глобальный разлогин при деплое.** Legacy plain-колонки (`refresh_token`, `parent_refresh_token`, `replaced_by_token`, `reset_password_token`) НЕ удалены (отдельная миграция позже), но перестали писаться; у `refresh_token` снят NOT NULL.
|
||||
- `shared/schema.ts` — новые колонки в `userSessions`/`users`; `resetPasswordTokenHash` добавлен в исключения `SafeUser`/`safeUserColumns`.
|
||||
- `server/storage/users.storage.ts` + интерфейс `server/storage.ts` — все lookup/отзыв сессий (`getUserSessionByToken`, `getUserSessionByParentToken`, `revokeSession`, `deleteUserSession`) и `getUserByResetPasswordToken` ищут по `hashToken(plain)`; `createUserSession` принудительно обнуляет plain-колонки; `markSessionReplaced(sessionId, newToken)` — по id сессии (plain старого токена больше недоступен).
|
||||
- `server/services/auth.service.ts` — `createSession` и ротация пишут только хэши; ротация вынесена в `rotateFamilySession()`. Grace-period (гонка вкладок, 5 мин): раньше клиенту возвращался plain-токен активной сессии из БД — теперь он не хранится, поэтому активная сессия ротируется и клиент получает новую пару (поведение сходится, сессий чуть больше). Cookie access Max-Age берётся из `ACCESS_TOKEN_EXPIRY`.
|
||||
- `server/routes/auth.core.routes.ts` — forgot-password пишет `resetPasswordTokenHash` (+ `resetPasswordToken: null`), plain только в письме; reset-password очищает обе колонки.
|
||||
- `.env.example`, `server/swagger.ts` — дефолты/описания приведены к факту (access 15m, refresh 30d/90d).
|
||||
- `tests/token-security.test.ts` — 12 тестов: hashToken, выпуск по хэшу (plain не уходит в storage), ротация с parent-хэшем, невалидный токен, reuse detection → revokeSessionFamily, grace-ротация, logout, reset по хэшу/очистка/истёкший/невалидный.
|
||||
- Как проверялось: `npm run check` чисто (оба tsc); `npx vitest run` — 118/118; `npm run lint` — 0 errors; `npm run build` — собирается. Клиент не тронут: refresh реактивный по 401 (singleton `refreshSession` в queryClient + useOfflineSync), таймеров, завязанных на 30d, нет.
|
||||
- Влияние на поиск/UX: нет (только auth-контур).
|
||||
- Подводные камни:
|
||||
- **Деплой разлогинит всех** (миграция чистит `user_sessions`) — деплоить в окно согласованного простоя; после деплоя проверить re-login и refresh-ротацию вручную.
|
||||
- JWT детерминирован по (payload, iat в секундах): две ротации одной и той же сессии в пределах одной секунды выдадут одинаковый токен — безвредно (хэш и семантика совпадают).
|
||||
- `bot_sessions` (refresh-токены bot_login) сознательно не тронуты — отдельная модель audience `workflow-bots`; кандидат на аналогичное хэширование отдельным шагом. In-memory `resetTokenStore` суперадмина — не в БД, вне скоупа.
|
||||
- Удаление legacy plain-колонок — отдельная миграция после подтверждения стабильности на проде.
|
||||
|
||||
Reference in New Issue
Block a user