feat(auth): шаг 0.6 — TTL access 15 минут + хэширование refresh/reset-токенов в БД
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:
2026-09-08 11:35:32 +03:00
parent 639036c87e
commit 06bfd2aa75
11 changed files with 506 additions and 60 deletions

View File

@@ -7,7 +7,7 @@ import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth
import { validateRequest } from "../middleware/validation.middleware";
import { registerOrganizationSchema, loginSchema, registerByInvitationSchema } from "@shared/schema";
import { hashPassword, verifyPassword } from "../utils/password";
import { generateTokens } from "../utils/jwt";
import { hashToken } from "../utils/jwt";
import { emailService } from "../services/email.service";
import { notificationService } from "../services/notification.service";
import { authLimiter, refreshLimiter } from "./shared";
@@ -339,8 +339,10 @@ export function registerAuthCoreRoutes(router: Router): void {
const token = crypto.randomBytes(32).toString('hex');
const expires = new Date(Date.now() + 60 * 60 * 1000);
// Шаг 0.6: в БД сохраняется только sha256-хэш токена, plain — только в письме.
await storage.updateUser(user.id, {
resetPasswordToken: token,
resetPasswordToken: null,
resetPasswordTokenHash: hashToken(token),
resetPasswordExpires: expires,
});
@@ -381,7 +383,7 @@ export function registerAuthCoreRoutes(router: Router): void {
}
if (!user.resetPasswordExpires || new Date() > new Date(user.resetPasswordExpires)) {
await storage.updateUser(user.id, { resetPasswordToken: null, resetPasswordExpires: null });
await storage.updateUser(user.id, { resetPasswordToken: null, resetPasswordTokenHash: null, resetPasswordExpires: null });
return res.status(400).json({ success: false, error: 'Срок действия ссылки истёк. Запросите новую.' });
}
@@ -389,6 +391,7 @@ export function registerAuthCoreRoutes(router: Router): void {
await storage.updateUser(user.id, {
passwordHash,
resetPasswordToken: null,
resetPasswordTokenHash: null,
resetPasswordExpires: null,
});